Attestation Signing
HappyView can sign records with an ECDSA (secp256k1) keypair so their origin can be verified later. Lua scripts call sign on happyview.atproto to attach an inline signature to a record and verify_signature to check one. HappyView’s implementation follows the atproto attestation spec.
How it works
Section titled “How it works”- HappyView loads or generates a secp256k1 keypair on startup
atproto.sign(record)encodes the record to DAG-CBOR, computes its CID, and signs the CID with the private key- The signature is added to the record’s
signaturesarray as an inline object atproto.verify_signature(record, sig, repo_did)recomputes the CID and verifies the signature
The repo DID is included in the signed data — a signature for one user’s record can’t be replayed against another’s. Any modification to the record invalidates the signature.
Attestation signing is enabled by default — HappyView generates a keypair on first startup and persists it to the happyview_instance_settings database table. No configuration is required.
To use an explicit key instead, set the ATTESTATION_PRIVATE_KEY environment variable:
| Variable | Required | Default | Description |
|---|---|---|---|
ATTESTATION_PRIVATE_KEY | no | auto-generated | Hex-encoded 32-byte secp256k1 private key |
ATTESTATION_KEY_ID | no | did:web:{host}#attestation | Key identifier included in signatures. Derived from PUBLIC_URL by default |
ATTESTATION_SIG_TYPE | no | app-specific NSID | The $type value used in signature objects |
The key ID defaults to a did:web derived from your PUBLIC_URL. For example, PUBLIC_URL=https://happyview.example.com produces a key ID of did:web:happyview.example.com#attestation.
Priority order
Section titled “Priority order”HappyView checks for signing configuration in this order:
- Environment variables — if
ATTESTATION_PRIVATE_KEYis set, it’s used - Database — if previously generated keys exist in
happyview_instance_settings, they’re loaded - Auto-generation — a new key is generated and persisted to the database
If key loading fails for any reason, signing is disabled and both sign and verify_signature raise NO_SIGNER.
Using in Lua scripts
Section titled “Using in Lua scripts”Both functions live on the happyview.atproto library, available in every script kind.
Signing a record
Section titled “Signing a record”local time = require("internal.time")local record = require("happyview.record")local atproto = require("happyview.atproto")
function handle(input, ctx) local body = { text = input.text, createdAt = time.to_iso8601(time.now()) } local ref = record.create(ctx.collection, body)
local sig = atproto.sign(body) return { uri = ref.uri, cid = ref.cid, signature = sig }endThe returned signature object:
{ "$type": "your.app.attestation", "key": "did:web:happyview.example.com#attestation", "signature": { "$bytes": "base64-encoded-signature" }}sign signs as ctx.caller_did, since the DID is part of the signed content; it raises BAD_INPUT from a script with no caller.
Verifying a signature
Section titled “Verifying a signature”local db = require("happyview.db")local atproto = require("happyview.atproto")
function handle(input, ctx) local row = db.get(input.uri) if not row then return { error = "not found" } end local record = row.record
local sig = record.signatures and record.signatures[1] if not sig then return { record = record, verified = false } end
local ok, valid = pcall(atproto.verify_signature, record, sig, row.did) if not ok then -- Couldn't check — that is not the same as "the signature is bad". return { record = record, verified = nil, error = tostring(valid) } end return { record = record, verified = valid }endverify_signature returns false only when it checked the signature and it didn’t match. It raises UNVERIFIABLE when it couldn’t check — signature bytes that aren’t valid base64, a missing field, a record that won’t encode. Keep those apart: a script that treats “couldn’t check” as false will report a fault in its own verification path as a forged record, which is a serious thing to tell a user about their own data.
Checking availability
Section titled “Checking availability”Both functions raise NO_SIGNER when no signer is configured. A script that should work either way wraps the call:
local ok, sig = pcall(atproto.sign, body)if ok then body.signature = sigendSignature format
Section titled “Signature format”Signatures are stored as objects in the record’s signatures array:
| Field | Type | Description |
|---|---|---|
$type | string | Signature type NSID |
key | string | Key identifier (DID with fragment) |
signature | table | Contains $bytes (base64-encoded) |
Security considerations
Section titled “Security considerations”sign exposes the instance’s signing key to Lua scripts. Treat it as a
privileged capability:
- It signs exactly what you give it. A signature only proves “this HappyView instance signed this content” — it does not prove the content is authentic, is present in anyone’s repo, or was authored by any particular DID. Only sign content you have already verified.
- It’s available to any script with a caller, including a record-event script triggered by an arbitrary firehose record, where the caller is the record’s repo DID. Don’t sign untrusted input (a firehose record, a request parameter) unless you intend the instance to vouch for it.
- Installing
happyview.atprotograntsattest:sign, and creating scripts requires thescripts:managepermission. The signing key is therefore only reachable by operators you have trusted with that permission — grant it accordingly, and review scripts that callsign.
If you need signatures scoped to a specific verified subject, have the script
verify the subject itself (e.g. confirm the record’s did and content against the
source) before calling sign.
Next steps
Section titled “Next steps”- Libraries —
happyview.atprotoand where its full surface is documented - Signed Record — save a record with an attestation signature
- Verify Signed Record — fetch a record and verify its signature

