- Guides
- Recipes
Verify a signed record
Fetch a record and verify its attestation signature.
Trigger: xrpc.query:<nsid>. Needs: happyview-db, happyview-atproto.
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 = "RecordNotFound", message = "no record at that uri" } end local record = row.record
local verified = false if record.signature then local ok, result = pcall( atproto.verify_signature, { text = record.text, createdAt = record.createdAt }, record.signature, row.did ) if not ok then -- We could not check. Say so, rather than reporting a bad signature. return { record = record, verified = nil, verifyError = tostring(result) } end verified = result end
return { record = record, verified = verified }endimport db from "happyview.db";import atproto from "happyview.atproto";
export default async function handle(input, ctx) { const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; } const record = row.record;
let verified = false; if (record.signature) { try { verified = await atproto.verify_signature( { text: record.text, createdAt: record.createdAt }, record.signature, row.did, ); } catch (err) { // We could not check. Say so, rather than reporting a bad signature. return { record, verifyError: String(err) }; } }
return { record, verified };}import db from "happyview.db";import atproto from "happyview.atproto";
export default async function handle(input: { uri: string }) { const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; } const record = row.record;
let verified = false; if (record.signature) { try { verified = await atproto.verify_signature( { text: record.text, createdAt: record.createdAt }, record.signature, row.did, ); } catch (err) { // We could not check. Say so, rather than reporting a bad signature. return { record, verifyError: String(err) }; } }
return { record, verified };}How it works
Section titled “How it works”- Fetch the record by AT URI. The envelope’s
didis the repo the signature was bound to. If there is no record, return aRecordNotFounderror. Declare it underdefs.main.errorsin the lexicon, as in"errors": [{ "name": "RecordNotFound" }], for the response to be a 400; an undeclared one is sent as a 200. - If a signature is present, rebuild the same field table that was signed and verify it with
verify_signatureonhappyview.atproto. - Return
verifiedastrueif the signature is valid, andfalseif it’s missing or doesn’t match.
verify_signature returns false only when it checked and the signature didn’t match. It raises when it couldn’t check at all: UNVERIFIABLE for unparseable signature bytes or a missing field, and NO_SIGNER when the instance has no key. The pcall (Lua) or try/catch (JavaScript and TypeScript) keeps that apart from a bad signature, because assigning it to verified would report a decode bug as a forged record. The script reports it as verifyError instead, with verified left unset.
const response = await fetch( "http://127.0.0.1:3000/xrpc/xyz.example.getPost?uri=at://did:plc:abc/xyz.example.post/3abc123",);const data = await response.json();const response = await fetch( "http://127.0.0.1:3000/xrpc/xyz.example.getPost?uri=at://did:plc:abc/xyz.example.post/3abc123",);const data = await response.json();let response = client .get("http://127.0.0.1:3000/xrpc/xyz.example.getPost") .query(&[("uri", "at://did:plc:abc/xyz.example.post/3abc123")]) .send() .await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/xrpc/xyz.example.getPost?uri=at://did:plc:abc/xyz.example.post/3abc123", nil)resp, err := http.DefaultClient.Do(req)curl "http://127.0.0.1:3000/xrpc/xyz.example.getPost?uri=at://did:plc:abc/xyz.example.post/3abc123"{ "record": { "text": "Hello world", "createdAt": "2026-04-30T12:00:00Z", "signature": { "$type": "games.gamesgamesgamesgames.attestation", "key": "did:web:happyview.example.com#attestation", "signature": { "$bytes": "..." } } }, "verified": true}Use case
Section titled “Use case”Pair this with the Signed record procedure to create a write-then-verify flow. verify_signature recomputes the CID from the same fields that were signed, so a change to either field between write and read fails the check.
See Attestation signing for setup and configuration.
© HappyProto

