How SkillAuthor is built.
Implementation overview of the Claude Agent Skills Registry: the stack, the invariants we enforce, the architecture behind cryptographic proof of authorship for .mdc and SKILL.md files, and what is and isn't built. Source-of-truth for developers evaluating whether to depend on the service.
Stack and runtime
- Runtime: Node.js 22, Next.js 16 App Router
- Database: PostgreSQL 16 (self-hosted, Drizzle ORM)
- Payments: Stripe Checkout, API version
2026-04-22.dahlia - PDF: @react-pdf/renderer 4.x, Times Roman + Helvetica, A4
- Email: Cloudflare Email Sending Beta for transactional mail
- Edge: Cloudflare DNS / CDN / TLS in front of a Caddy 2 instance terminating HTTP/2 + HTTP/3 with automatic Let's Encrypt
- Host: Hetzner Cloud, EU region
Client-side SHA-256 hashing
The browser computes SHA-256 over the exact file bytes using crypto.subtle.digest("SHA-256", arrayBuffer) before submission. This gives the user instant feedback and lets the upload form display duplicate matches without first uploading. The server computes its own SHA-256 on receipt and that value is what gets recorded on the certificate — the client-side hash is only an indication during upload.
const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf);
const hex = Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");Atomic registration number minting
Numbers are minted inside the Stripe webhook handler in a single transaction:
return db.transaction(async (tx) => {
const [row] = await tx
.select({ id: skills.id,
registrationNumber: skills.registrationNumber })
.from(skills)
.where(eq(skills.id, skillId))
.for("update")
.limit(1);
if (!row) return { outcome: "not_found" };
if (row.registrationNumber)
return { outcome: "already_finalized",
registrationNumber: row.registrationNumber };
const [counter] = await tx
.insert(registrationCounters)
.values({ year, lastSeq: 1 })
.onConflictDoUpdate({
target: registrationCounters.year,
set: { lastSeq: sql`${registrationCounters.lastSeq} + 1` },
})
.returning({ lastSeq: registrationCounters.lastSeq });
const registrationNumber =
`SA-${year}-${String(counter.lastSeq).padStart(6, "0")}`;
await tx.update(skills).set({
paymentStatus: "paid",
registeredAt: now,
registrationNumber,
}).where(eq(skills.id, skillId));
return { outcome: "finalized", registrationNumber };
});Properties:
- Two concurrent Stripe deliveries for the same session cannot both pass the
registrationNumber IS NULLcheck — the second is serialised behind the row lock and returnsalready_finalized. - The counter is incremented exactly once per registration. Numbers can have gaps (abandoned checkout sessions) but never duplicates.
- The handler returns
200 OKonalready_finalizedso Stripe stops retrying.
Webhook security and idempotency
Verification uses stripe.webhooks.constructEventAsync (SubtleCrypto under the hood, async-friendly). On failure the response is a generic 400 invalid_signature — the exception message is logged server-side but never echoed back, to avoid signature-introspection oracles.
5xx responses trigger Stripe's exponential retry (up to three days in live mode). Combined with the row-locked finalisation above, retries are safe.
Defence-in-depth IP allowlisting for Stripe's published webhook IPs is supported by an opt-in @stripeWebhook block in the Caddyfile.
PDF certificate generation
- Pipeline: Drizzle row lookup →
@react-pdf/rendererDocument →renderToBuffer→application/pdfstream - Typography: Times Roman + Times Italic for body; Courier for the registration number, hash, and timestamp blocks
- Symbol:
/public/symbol.png(512 × 512) embedded as a raster Image in the footer - QR code: generated inline via the
qrcodelibrary at 256 × 256, error correction level M, black-on-white, embedded above “Verify at” on every certificate - Cache:
Cache-Control: public, max-age=31536000, immutable— each PDF URL is content-addressed by registration number, so immutability is safe
QR code on every certificate
Every certificate carries a QR that encodes https://skillauthor.com/v/<reg>. The same QR is available as a 1024 × 1024 standalone PNG at /api/certificate/<reg>/qr.
Specs:
- Error correction: M (≈ 15 %), enough redundancy for print and paper damage
- Module size: 32 px at 1024 × 1024
- Quiet zone: 1 module
- Colours:
#000000on#ffffff— scanner compatibility first
Independent verification
Public verification needs only shasum and the registration number:
$ shasum -a 256 inspector-agent.mdc
# compare to the fingerprint at /v/SA-2026-000142
# or programmatically:
$ curl -s https://skillauthor.com/api/register/check \
-H 'Content-Type: application/json' \
-d '{"sha256":"<your hash>"}'The verification page at /v/<reg> does not require accounts, cookies, or JavaScript.
Data model and privacy
- No user accounts. No password storage, no session cookies, no OAuth.
- Email is never published. Stored only for receipt delivery and verification correspondence.
- Web logs retained 30 days for security and abuse-prevention; deleted on a rolling basis.
- Registration records retained indefinitely — that's the service. GDPR erasure requests replace the author name with “Author withdrawn” but preserve the hash, number, and timestamp.
See /privacy for the full data inventory.
Semantic near-duplicate detection (active)
After the registration is sealed, the webhook schedules a background check between 5 and 60 minutes later. The check is owned by a cron-invoked endpoint POST /api/jobs/near-dup-check; the request path never blocks on it, so checkout and PDF issuance are unaffected by OpenAI latency or outages.
- Model:
text-embedding-3-small· 1536 dimensions · input truncated to 24 000 chars - Index: pgvector
ivfflatwithvector_cosine_ops, lists = 100 - Default alert threshold: cosine similarity ≥ 0.84. The threshold is per-receiver and raised by 0.04 (capped at 0.92) when a recipient flags an alert as a false positive
- Only the older skill in a strongly-similar pair receives an alert — never the new submitter
- Top 3 strongest matches surface per scan. Every match becomes an alert row with a per-alert false-positive token; recipients are deduplicated so a single registrant who owns multiple matched skills receives one email, not several
- Retry policy: 5 attempts with a 1-hour lease between attempts; non-retryable errors (e.g. missing API key) move the task straight to
failed - Failure mode: if OpenAI is unreachable, the task is rescheduled an hour later; the registration itself is unaffected
Roadmap
Considered, not started. We will not advertise them on the homepage until they ship.
- Signed JSON attestations. A .json companion to the PDF, signed with SkillAuthor's Ed25519 key. Verifiable without the PDF rendering pipeline. Public key published at
/.well-known/skillauthor.pub. - Authenticated per-author API. “List my registrations”, transfer ownership, revoke. Requires accounts; intentionally deferred.
- Hash-only private tier. Pay to publish only the hash, not the bytes. Schema already has
is_public; only the UI toggle and the registry filter are missing. - Merkle-rooted snapshots. Daily hash of
(sha256, registered_at)rows, anchored in a public timestamp service (OpenTimestamps or a public git mirror). Long-term archival guarantee.
No date promises. Email [email protected] if you depend on a specific item — that's how priority is assigned.
