Step-by-step guide to take the DMV agent registration system from dev to production.
- Supabase CLI installed (
brew install supabase/tap/supabase) - Logged in to Supabase (
supabase login) - Linked to the agentcommunity project (
supabase link --project-ref tcymqfwwphacnosnnzxl) - npm owner access for both
@agentcommunity/dmv-agentand the existingdmv-agentcompatibility alias
The registrations table should already exist in the shared agentcommunity Supabase project. Verify it has the right schema:
-- Required columns (check in Supabase dashboard → Table Editor)
registration_type TEXT -- 'INDIVIDUAL' | 'ORGANIZATION' | 'AGENT'
full_name TEXT -- nullable (required for INDIVIDUAL/ORGANIZATION)
organization_name TEXT -- nullable (required for ORGANIZATION)
domain_requested TEXT -- e.g. 'my-agent.agent' (NOT unique — pre-registration model)
email TEXT
certificate_id TEXT -- UNIQUE partial index (WHERE certificate_id IS NOT NULL)
signup_source TEXT -- 'ui' | 'cli' | 'mcp' | 'api'
status ENUM -- NOT set by register-agent; uses DB default 'pending_profile'
user_id UUID -- nullable (set by trigger, NOT by register-agent)
metadata JSONB -- { agent_description, client_ip_hash }; raw IPs are never stored
created_at TIMESTAMPTZ -- default now()domain_requestedis NOT unique — multiple users can pre-register the same.agentdomaincertificate_idhas a UNIQUE partial index — prevents same user re-registering the same agentuser_idis nullable — register-agent INSERTs with NULL, theon_dmv_registrationtrigger fills it in- CHECK constraints enforce
full_namefor INDIVIDUAL/ORGANIZATION andorganization_namefor ORGANIZATION metadata.client_ip_hash, when present, is one-way SHA-256 hex. Never add a raw client IP field or use the shared database as an IP-rate-limit fallback.
On INSERT where certificate_id IS NOT NULL, the on_dmv_registration trigger fires asynchronously (pg_net) and calls the handle-dmv-registration edge function on the agentcommunity.org side. This:
- Creates or finds an auth user by email
- Sends a magic link (new users only)
- Upserts the domain into
user_domains - Sends a certificate email with badge embed codes
- Links the DMV registration row for new users, or preserves the existing-user DMV row as audit evidence with
user_idleft null
DMV does NOT create auth users, send emails, or write to user_domains.
-- Anon users should NOT have direct access (all writes go through edge function)
-- The edge function uses the service role key, which bypasses RLS
ALTER TABLE registrations ENABLE ROW LEVEL SECURITY;
-- No policies = deny all for anon roleThree edge functions live in supabase/functions/:
| Function | Method | Purpose |
|---|---|---|
register-agent |
POST | Worker upstream — validates, lifetime cap, generates cert, INSERTs. Anti-abuse lives in the worker. |
lookup-agent |
GET | Internal Worker upstream — secret-gated lookup by certificate ID only |
badge |
GET | SVG badge generator (flat for READMEs, card for websites) |
The certificate-lookup Worker and Edge changes are live as of 2026-07-22.
Merged main fabafe6 (PR #20, including the manual-redirect runtime fix) is
deployed as Worker version d9755e66-3883-4970-be84-a59307011f14, created
2026-07-22T12:01:52.501Z. The rollout record below is retained as the required
order and recovery checklist for future changes.
DMV_PROXY_SECRET must be the same generated secret on the Cloudflare Worker
and the Supabase project. Never put its value in .env.example, documentation,
shell history, or client configuration. The Worker also requires
RL_CERT_LOOKUP (coarse 60/60), CERT_LOOKUP_LIMITER (exact SQLite DO 30/60),
and BADGE_CACHE_KV (lookup result cache) as configured in wrangler.jsonc.
Registration also requires REGISTER_FINGERPRINT_LIMITER, the exact SQLite
Durable Object budget introduced by forward-only v3. The legacy
REGISTER_COOLDOWN_KV binding is retained but unused during rollout.
Current production status (2026-08-02): v3 is now live. A non-main branch
was accidentally promoted to production on 2026-08-02 because Cloudflare's
non-production branch command ran a production deploy. The dashboard has been
corrected and verified: production uses npx wrangler deploy; non-production
uses npx wrangler versions upload. The repository guard also selects from
WORKERS_CI_BRANCH, uploads non-main Workers Builds, and rejects local
non-main/detached production attempts. pnpm cf:preview is the explicit
upload-only local path.
Because v3 reached production, the Durable Object migration is forward-only
operational state even if the promotion itself was accidental. Preserve
v1/v2/v3 and their classes, exports, bindings, and migrations in main and in
every recovery Worker. Do not deploy a pre-v3 main. The completed 2026-07-22
evidence below remains lookup v2 evidence; do not mislabel it as the separate v3
registration smoke record.
The v3 source uses a closed completion classification. Only a well-formed fresh
201 commits a fingerprint claim. Only well-formed pre-INSERT 400/403/409
responses and an exact 200 already_recorded replay release one. Every 5xx/546,
unexpected or malformed response, body-read failure, timeout/abort, or transport
failure stays pending. The 45-second Worker response timeout is local only and
does not claim to cancel Supabase execution. A pending claim is converted to a
possible-success timestamp at claim time + 600 seconds, then held for the full
24-hour rolling window. That 600-second horizon covers Supabase's documented
150-second request-idle timeout, 400-second Edge Function wall-clock duration,
and a 50-second safety margin. Current primary references:
functions/limits and
Workers Request.signal.
Before merging, run these container gates on a Docker-capable machine:
docker info
pnpm cf:container:buildAny Docker CLI error text is a failed gate even if a wrapper later exits zero. The 2026-07-22 implementation host has no Docker/Podman runtime, so these gates and the real Worker rollout must run in Cloudflare's build environment or another Docker-capable environment.
The completed rollout used the order below. Final production results were:
REEF-068-BD0Q→200 issued(masato);ZZZZ-FFF-FFFD→200 not_found;INVALID→400.- Exact limiter calls 1–30 were allowed; call 31 returned
429with remaining0; a next-minute call returned200with remaining29. - Secretless direct
lookup-agentaccess returned403 direct_access_deprecated. /healthzreturned JSON200; badge returned SVG200; permalink returned HTML200; card returned PNG200; validation-onlyGET /api/registerreturned405.- No Supabase registration or member rows were deleted or mutated during verification. The limiter and result-cache smokes intentionally wrote Durable Object/KV operational state.
- Before merging, record the target feature-branch SHA and the currently
deployed production Worker SHA/version in the launch notes. In the
Cloudflare account, confirm that native rate-limit namespace
1002is allocated toRL_CERT_LOOKUPand does not collide with another account-wide binding. ConfirmBADGE_CACHE_KV,CERT_LOOKUP_LIMITER,REGISTER_FINGERPRINT_LIMITER, all forward-only v1/v2/v3 Durable Object migrations, and the sharedDMV_PROXY_SECRETare configured without printing the secret. The v3 class and binding must remain together in every deployment now that registration traffic has reached the v3 Worker. - Merge to
main, record the resulting mergedmainSHA, and use the Cloudflare Git integration's automatic build as the single authoritative Worker deploy path. Watch that build and capture its deployed commit SHA/version. If no automatic build starts, first confirm in the dashboard that an automatic build/deploy is neither active nor already started, then usepnpm cf:deployonce as the fallback. Never run the automatic and manual paths concurrently. - Before changing Supabase, smoke
/healthz, an existing card and badge, validation-only registration, invalid lookup (400), and a known valid-format lookup. Only on a first publication against a legacy Edge contract, the valid-format lookup may briefly be fail-closed503 unavailablebefore Step 4. On future changes to the already-live Edge contract, expect the current typed result instead; treat any unexpected503as a failure to investigate, not as an expected rollout state. - Deploy only the changed lookup function:
supabase functions deploy lookup-agent --project-ref tcymqfwwphacnosnnzxl --no-verify-jwtDo not redeploy register-agent or badge during this rollout. Supabase must
bypass its platform JWT layer because the Worker authenticates with
x-dmv-proxy. Supabase automatically injects SUPABASE_URL and
SUPABASE_SERVICE_ROLE_KEY.
- Prove issued and generated-valid-but-absent results through the Worker, the
invalid-format response, exact 30/60 limiting, and a secretless direct
lookup-agentrequest returning403. Also re-run health, card, badge, and registration smokes. Keepwrangler tailor the Cloudflare Durable Object metrics open: capture first provisioning/storage, the 31st-request denial, an alarm event/metric after expiry, and a post-window request with 29 remaining. This is the real deployed v2 storage/alarm smoke. - Only after those results pass, update every active status surface with the
new evidence:
README.md,llms.txt,index.html,CLOUDFLARE.md,AUTH_DMV.md,packages/dmv-agent/DEPLOY.md,AGENT_HANDOFF.md,AGENTS.md,CLAUDE.md,ARCHITECTURE.md,SECURITY.md, andpackages/dmv-agent/README.md. Commit and push that evidence-backed status update. Do not declare launch complete yet. - Public docs/assets are included in the Worker bundle, so the live-status
commit triggers a second automatic production deployment. Watch the build for
that exact status commit start and finish. Capture its commit SHA, final Worker
version/deployment ID, timestamps, and result. If no automatic build starts,
first confirm that an automatic build/deploy is neither active nor already
started. Only then use
pnpm cf:deployonce as fallback; never run the two paths concurrently. - Against that final status-commit deployment, re-run at minimum one issued or
typed-not-found lookup plus
/healthz, an existing card, an existing badge, and validation-only registration. Record their statuses/results. Only after these final smokes pass may the rollout be handed off or declared live.
Cloudflare's SQLite Durable Object migrations are forward-only operational
state. Preserve the deployed v1/v2/v3 classes, migrations, exports, and
bindings. Never use Cloudflare rollback to a pre-v3 Worker: such a
version lacks the RegistrationFingerprintRateLimiter export/binding while the
account retains the migration. Preserve v1 CardRenderer, v2
CertificateLookupRateLimiter, v3 RegistrationFingerprintRateLimiter, and
all corresponding bindings in every recovery version.
Cloudflare's runtime rejects redirect: 'error'. Keep the internal upstream
fetch at redirect: 'manual'; any 3xx remains fail-closed and must never be
followed with the shared secret.
- If the Worker fails before the Edge deploy, stop. Restore traffic by shipping a new compatible Worker version that retains the migration/export/bindings; prefer roll-forward. Do not deploy the Edge function yet.
- If the Edge deploy fails after its secret gate is active, the Worker safely
returns uncached
503 unavailable. Roll forwardlookup-agent; do not restore the legacy Edge contract or direct public access. - Record the prior/current deployed versions and the exact failed smoke state before remediation. Do not delete or reverse Durable Object migrations or storage.
The canonical path is the Cloudflare Worker, not the Supabase URL directly:
# Read-only public surface check: healthz, card PNG, badge SVG, validation-only register 400
dmv-agent doctor
# Machine-readable variant for launch automation; same checks and exit code
dmv-agent doctor --json# Worker (canonical) — requires machine_fingerprint for non-browser traffic
curl -X POST https://dmv.agentcommunity.org/api/register \
-H 'Content-Type: application/json' \
-d '{
"agent_name": "test-deploy",
"email": "test@example.com",
"operator_name": "Test",
"registration_type": "AGENT",
"signup_source": "cli",
"machine_fingerprint": "test_fingerprint_64chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}'Expected: 201 with certificate_id, agent_name, domain, registration_type, permalink_url, badge_url, badge_card_url, message.
Direct-to-Supabase calls no longer work — they return 403 (see the post-deploy secret-gate check below).
The x-dmv-proxy secret gate closes the direct-Supabase bypass. Unit tests cover
both internal upstreams. After a deployment, retain the registration negative
smoke below without ever sending the real secret; do not publish or copy the
internal lookup URL into client-facing instructions:
# No header → rejected
curl -i -X POST https://tcymqfwwphacnosnnzxl.supabase.co/functions/v1/register-agent \
-H 'Content-Type: application/json' -d '{}'
# Expect: 403 (direct_access_deprecated)
# Retired public constant → rejected
curl -i -X POST https://tcymqfwwphacnosnnzxl.supabase.co/functions/v1/register-agent \
-H 'Content-Type: application/json' -H 'x-dmv-proxy: v1' -d '{}'
# Expect: 403 (the `v1` constant was retired 2026-05-29; only the DMV_PROXY_SECRET shared secret is accepted)
A legit registration and lookup must still succeed through the Worker at
/api/register and /api/lookup. If either direct test above does not return 403,
the secret gate is misconfigured — do not consider the deploy complete.
# Missing fields → 400
curl -X POST .../api/register -H 'Content-Type: application/json' -d '{}'
# Invalid agent name → 400
curl -X POST .../api/register -H 'Content-Type: application/json' \
-d '{"agent_name": "AB", "email": "x@y.com"}'
# CLI/MCP missing machine_fingerprint → 400 machine_fingerprint_required
curl -X POST .../api/register -H 'Content-Type: application/json' \
-d '{"agent_name": "x", "email": "x@y.com", "signup_source": "cli"}'
# Browser missing Turnstile token → 400 turnstile_required
curl -X POST .../api/register -H 'Content-Type: application/json' \
-d '{"agent_name": "x", "email": "x@y.com", "signup_source": "ui"}'
# Duplicate certificate_id → 200 recovery (same user + same agent name + same type)
# Returns the existing certificate payload with already_recorded=true
# Rate limit → 429 with Retry-After: 60
# Triggers: 6+ registrations to the same email within 60s, OR
# 5+ registrations to the same (IP, email) combo within 60sBASE=https://dmv.agentcommunity.org
# After the Edge deploy: issued or not_found through the Worker → 200 JSON
curl "$BASE/api/lookup?id=MESA-DD6-660J"
# Direct secretless Edge access must be closed → 403
curl -i \
"https://tcymqfwwphacnosnnzxl.supabase.co/functions/v1/lookup-agent?id=MESA-DD6-660J"
# Domain enumeration is removed; do not send requested names to this endpoint.
# Invalid cert → 400
curl "$BASE/api/lookup?id=FAKE-000-0000"
# Flat badge SVG (for GitHub READMEs)
curl "$BASE/badge?id=MESA-DD6-660J" -o badge.svg
# Card badge SVG (for websites)
curl "$BASE/badge?id=MESA-DD6-660J&style=card" -o badge-card.svg
# Badge lookup is also certificate-ID-only; do not send requested names.The lookup Worker applies the permissive/eventually consistent native 60/60
filter first, then exact atomic 30 requests per 60 seconds per hashed IP through
CERT_LOOKUP_LIMITER. Issued results are
cached internally for 300 seconds and not-found results for 60 seconds, but client
responses are private, no-store. Results contain only certificate_id,
status, valid_format, issued, agent_name, and certificate_url.
issued: true means a matching registration row exists; it does not mean email
verification, name allocation, or DNS delegation completed. Only the internal
typed HTTP 200 not_found envelope is cached as absence; non-200 or malformed
upstream results are uncached unavailable.
After registration, users get these snippets:
GitHub README (Markdown):
[](https://dmv.agentcommunity.org/c/MESA-DD6-660J/my-assistant)Website (HTML):
<a href="https://dmv.agentcommunity.org/c/MESA-DD6-660J/my-assistant">
<img src="https://dmv.agentcommunity.org/badge?id=MESA-DD6-660J&style=card" alt="my-assistant.agent — DMV Certificate" />
</a>In js/supabase.js, flip the feature flag:
export const SUPABASE_ENABLED = true; // was falseThen bump the cache-busting version:
js/supabase.jsimport inapp.js:?v=N→?v=N+1index.htmlscript tag:?v=N→?v=N+1
The registry currently contains canonical @agentcommunity/dmv-agent@0.2.2
and compatibility alias dmv-agent@0.1.2. Source 0.3.0 and alias 0.1.3
are release candidates, not published releases. npm versions are immutable, so
do not publish either candidate manually or before all gates pass.
An npm owner must configure the same exact GitHub trusted publisher for each
package: organization agentcommunity, repository DMV_for_agents, workflow
filename publish-dmv-packages.yml, environment npm-production, and allowed
action npm publish. In GitHub, create that exact protected environment with
required reviewers and a deployment branch rule allowing main only. The
workflow also enforces refs/heads/main. Keep these repository, workflow, and
environment values identical for both packages.
Unprivileged jobs run all candidate checkout/build/test/pack/state/proof code.
Only the two minimal publish jobs receive id-token: write; they use the
protected environment, download the already-verified run artifact, and execute
one exact npm publish <tgz> --access public --provenance command. Verifier
child processes scrub GitHub OIDC request variables and npm credential
variables. The workflow uses GitHub-hosted runners, exact Node 24.18.1 and npm
12.0.2, and no dependency cache. npm's current trusted-publishing minimums are
Node 22.14 and npm 11.5.1. Do not add NPM_TOKEN or NODE_AUTH_TOKEN.
Before authorizing the workflow, run from the repository root:
pnpm install --frozen-lockfile
pnpm test
pnpm build
pnpm cf:build
pnpm exec tsc --noEmit
pnpm verify:packages -- --registry-mode=currentpnpm verify:packages is the executable package evidence gate. It reads both
source manifests, rejects package-local lockfiles (the root
pnpm-lock.yaml is the sole authority), builds from clean source, compares
npm pack --dry-run --json with actual canonical/alias archives, enforces
exact allow-lists, exact README/CHANGELOG/LICENSE bytes, and no secret/source/
test leakage. It clean-installs with scripts disabled, invokes both aliases of
the CLI plus the canonical MCP server, checks bounded registry requests and
tarball integrity. Its default mode performs no production operation. Run
pnpm verify:packages -- --registry-mode=current --production-smoke separately
only when live evidence is intended. That opt-in performs doctor, issued lookup,
and exact secretless registration/lookup-gate checks. It never submits a
registration or writes business data, but it consumes live rate-limit/lookup
quota and may populate caches. Every mode removes its generated package dist.
Dispatch Publish DMV npm packages only with the exact confirmation string.
The workflow produces and uploads two verified tarballs once. For each immutable
version, a registry 404 is absent and publishes; a present candidate must match
the exact local SRI and pass full proof, then skips safely. A different or
ambiguous result fails with version bump required. This makes reruns safe after
canonical-only publication and after alias completion without repacking inside
an OIDC job. Canonical exact proof completes before alias state or publication.
The final gate requires the alias dependency range to be exactly
@agentcommunity/dmv-agent@^0.3.0 and proves SRI/downloaded bytes, gitHead,
DSSE/in-toto/SLSA type, GitHub-hosted builder, exact repo/workflow/main ref,
resolved source commit, and package subject digest. npm audit signatures
cryptographically verifies signatures, attestations, and transparency evidence;
registry signatures remain distinct from provenance.
Trusted-publisher setup and workflow dispatch are external owner actions. No package is published merely by merging this source. After the first successful OIDC release, restrict traditional token publication and revoke obsolete automation tokens only after confirming the trusted publisher works.
Primary release references checked 2026-08-01:
- npm trusted publishing
- npm publish and pack contents
- GitHub deployment environments and protection rules
- GitHub OIDC job permissions
- Node.js release status
- GitHub
setup-node
# In a fresh directory
bunx @agentcommunity/dmv-agent verify MESA-DD6-660J
# Should print: ✓ Certificate MESA-DD6-660J has a valid check digit.
bunx @agentcommunity/dmv-agent register
# Should prompt for agent name, email, etc.
# Should succeed if edge function is deployedAdd to a test project's .claude/settings.json:
{
"mcpServers": {
"dmv": {
"command": "bunx",
"args": ["@agentcommunity/dmv-agent"]
}
}
}Open Claude Code, ask it to run a DMV readiness check → should call dmv_doctor and report OK after the invalid-payload validation check. Then ask it to register an agent → should call register_agent tool → should succeed.
mkdir -p .claude/skills
cp -r node_modules/@agentcommunity/dmv-agent/skills/dmv .claude/skills/Type /dmv in Claude Code → should guide through registration via CLI.
- Cloudflare dashboard → Workers → dmv-agentcommunity → Logs —
/api/registerinvocation count, error rate, Turnstile siteverify failures - Cloudflare dashboard → Workers → dmv-agentcommunity → Analytics Engine →
dmv_worker_events— query events wherecategory = 'register'and group by the tier blob. Actual tier values:405,validation,invalid_json,turnstile_required,turnstile_failed,machine_fingerprint_required,rate_limited,fingerprint_cooldown,supabase(successful forward, 2xx upstream), andsupabase_<status>(forwarded but upstream returned a non-2xx, e.g.,supabase_403for lifetime-cap,supabase_500for DB errors) - Supabase dashboard → Edge Functions → register-agent — invocation count, error rate, latency (now strictly worker-forwarded — direct access returns 403 via the
x-dmv-proxygate) - Supabase dashboard → Table Editor → registrations — row count, any anomalies
- Rate limiting — shared CF limits are 5/email/60s and 4/(IP+email)/60s. Adjust in
wrangler.jsoncratelimitsarray, but remember the namespace IDs are shared withagentCommunity_PAGE— coordinate with that repo before changing values. - Duplicate certs —
already_recorded=truemeans the same user tried to re-register the same agent (expected, they get their cert ID back). This is not a taken-name state;domain_requestedremains non-unique.
| Symptom | Cause | Fix |
|---|---|---|
Network error: could not reach DMV registration service |
Worker not deployed, Cloudflare DNS issue, or Supabase down | Check the authoritative Cloudflare Git build and pnpm cf:tail. Use pnpm cf:deploy only after confirming an automatic build/deploy is neither active nor already started; never run them concurrently. |
turnstile_failed (400) on browser |
Wrong TURNSTILE_SECRET_KEY on the worker, or stale token |
Check Cloudflare dashboard → Workers → dmv-agentcommunity → Variables and Secrets → confirm TURNSTILE_SECRET_KEY is encrypted Secret type |
machine_fingerprint_required (400) on CLI |
CLI on old version not sending fingerprint | Bump CLI dependency to latest @agentcommunity/dmv-agent |
Registration failed (HTTP 500) |
Service role key not set or DB schema mismatch | Check Supabase dashboard → Edge Functions → Logs |
already_recorded=true |
Same user re-registering same agent | Expected — returns cert ID + permalink for recovery |
Rate limited (429) |
Too many registrations from same email or (IP, email) within 60s | Wait 60s. Check shared CF rate limit counters in Cloudflare dashboard. |
fingerprint_cooldown (429) |
CLI/MCP machine fingerprint exhausted the exact Durable Object budget | Wait retry_after_seconds. Inspect REGISTER_FINGERPRINT_LIMITER; never expose object claim IDs or raw fingerprints. |
CLI hangs on bunx |
Package not published or npm registry cache | Try npx @agentcommunity/dmv-agent or bunx --force |
These are noted for future work, not needed for go-live:
- Link/visit tracking — Track permalink visits (
/c/CERT-ID/agent-name) to measure sharing virality. Needs: acard_viewstable (cert_id, viewer_ip_hash, referrer, user_agent, timestamp), a lightweight edge function or analytics endpoint, and client-side fire-and-forget POST on permalink load. This is critical for understanding card sharing conversion (view → "Get Yours" click → registration). - Email verification flow — Magic link sent by agentcommunity.org trigger (on_dmv_registration). New users get magic link + certificate email. Existing users get certificate email only.
- Google/GitHub OAuth — alternative to email verification
- Live lookup hardening: domain lookup is removed from
lookup-agentto reduce enumeration exposure. Public verification is certificate-ID-only through Worker/api/lookup; the coarse 60/60 filter plus exact Durable Object 30/60 limit mitigate rather than eliminate enumeration risk. The internal response is a typed HTTP 200issued/not_foundunion. - Badge by cert ID —
badgeedge function (domain lookup deprecated) - Real OG images — server-side card rendering for social media previews (front face of HoloCard as static PNG)
- Python SDK — thin wrapper that shells out to
bunxfor cross-language support - Admin dashboard — view registrations, manage verifications, handle disputes
User's machine Cloudflare Worker Supabase cloud
────────────── ────────────────────── ──────────────
┌───────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ Claude Code │ │ /api/register │ │ register-agent (DMV) │
│ /dmv skill │──▶│ validate JSON │──forward─▶│ validate (again) │
│ MCP tool │ │ require fingerprint │ │ lifetime cap (DB) │
└───────────────────┘ │ shared CF limits │ │ generate cert, INSERT │
│ exact fingerprint DO │ └──────────┬───────────┘
┌───────────────────┐ │ claim/complete │ │
│ Web UI │──▶│ │ │
│ js/supabase.js │ │ (browser path: │ │
└───────────────────┘ │ Turnstile siteverify│ │
│ before counters) │ │
│ │ ▼
│ 🔑 TURNSTILE_SECRET │ ┌────────────────────────┐
└──────────────────────┘ │ Supabase DB │
│ registrations table │
└──────────┬─────────────┘
│ AFTER INSERT trigger
│ (certificate_id IS NOT NULL)
▼
┌────────────────────────┐
│ handle-dmv-registration │
│ (agentcommunity.org) │
│ → create/find auth user │
│ → magic link (new user) │
│ → upsert user_domains │
│ → certificate email │
└────────────────────────┘
┌───────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ GitHub README │──▶│ /badge/* worker │──proxy──▶│ badge (DMV) │
│ <img src=badge> │ │ (KV cache + header │ │ SVG by cert ID only │
└───────────────────┘ │ hygiene) │ └──────────┬───────────┘
└─────────────────────┘ │ reads
┌───────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ Any HTTP client │───▶│ /api/lookup Worker │──secret──▶│ lookup-agent (DMV) │
│ curl, agents, etc │ │ 30/min/IP + cache │ │ cert ID only │
└───────────────────┘ │ six public fields │ │ direct access 403 │
└─────────────────────┘ └──────────────────────┘
Zero secrets in client code. The Cloudflare Worker holds the Turnstile secret;
Supabase holds the service role key. The worker is the public anti-abuse choke
point for /api/register and /api/lookup; the Edge Functions are
secret-gated internal upstreams.