Skip to content

Latest commit

 

History

History
596 lines (479 loc) · 32.5 KB

File metadata and controls

596 lines (479 loc) · 32.5 KB

DMV Agent — Go Live Checklist

Step-by-step guide to take the DMV agent registration system from dev to production.


Prerequisites

  • 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-agent and the existing dmv-agent compatibility alias

1. Database — Supabase

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()

Key constraints

  • domain_requested is NOT unique — multiple users can pre-register the same .agent domain
  • certificate_id has a UNIQUE partial index — prevents same user re-registering the same agent
  • user_id is nullable — register-agent INSERTs with NULL, the on_dmv_registration trigger fills it in
  • CHECK constraints enforce full_name for INDIVIDUAL/ORGANIZATION and organization_name for 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.

Trigger chain (managed by agentcommunity.org)

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_id left null

DMV does NOT create auth users, send emails, or write to user_domains.

RLS policies

-- 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 role

2. Public API and Edge Functions — Deploy

Three 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:build

Any 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.

Completed rollout evidence

The completed rollout used the order below. Final production results were:

  • REEF-068-BD0Q200 issued (masato); ZZZZ-FFF-FFFD200 not_found; INVALID400.
  • Exact limiter calls 1–30 were allowed; call 31 returned 429 with remaining 0; a next-minute call returned 200 with remaining 29.
  • Secretless direct lookup-agent access returned 403 direct_access_deprecated.
  • /healthz returned JSON 200; badge returned SVG 200; permalink returned HTML 200; card returned PNG 200; validation-only GET /api/register returned 405.
  • 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.

Required rollout order

  1. 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 1002 is allocated to RL_CERT_LOOKUP and does not collide with another account-wide binding. Confirm BADGE_CACHE_KV, CERT_LOOKUP_LIMITER, REGISTER_FINGERPRINT_LIMITER, all forward-only v1/v2/v3 Durable Object migrations, and the shared DMV_PROXY_SECRET are 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.
  2. Merge to main, record the resulting merged main SHA, 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 use pnpm cf:deploy once as the fallback. Never run the automatic and manual paths concurrently.
  3. 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-closed 503 unavailable before Step 4. On future changes to the already-live Edge contract, expect the current typed result instead; treat any unexpected 503 as a failure to investigate, not as an expected rollout state.
  4. Deploy only the changed lookup function:
supabase functions deploy lookup-agent --project-ref tcymqfwwphacnosnnzxl --no-verify-jwt

Do 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.

  1. Prove issued and generated-valid-but-absent results through the Worker, the invalid-format response, exact 30/60 limiting, and a secretless direct lookup-agent request returning 403. Also re-run health, card, badge, and registration smokes. Keep wrangler tail or 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.
  2. 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, and packages/dmv-agent/README.md. Commit and push that evidence-backed status update. Do not declare launch complete yet.
  3. 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:deploy once as fallback; never run the two paths concurrently.
  4. 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.

v3-safe recovery

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 forward lookup-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.

Verify it's running

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).

Post-deploy verification — secret gate

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.

Test error cases

# 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 60s

Verify lookup & badge

BASE=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.

Badge embed codes

After registration, users get these snippets:

GitHub README (Markdown):

[![my-assistant.agent](https://dmv.agentcommunity.org/badge?id=MESA-DD6-660J)](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>

3. Web UI — Enable Supabase

In js/supabase.js, flip the feature flag:

export const SUPABASE_ENABLED = true;  // was false

Then bump the cache-busting version:

  • js/supabase.js import in app.js: ?v=N?v=N+1
  • index.html script tag: ?v=N?v=N+1

4. NPM Package — Publish

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=current

pnpm 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:

Verify it works

# 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 deployed

MCP server test

Add 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.

Skill test

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.


5. Post-Launch Monitoring

Things to watch

  • Cloudflare dashboard → Workers → dmv-agentcommunity → Logs/api/register invocation count, error rate, Turnstile siteverify failures
  • Cloudflare dashboard → Workers → dmv-agentcommunity → Analytics Engine → dmv_worker_events — query events where category = '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), and supabase_<status> (forwarded but upstream returned a non-2xx, e.g., supabase_403 for lifetime-cap, supabase_500 for 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-proxy gate)
  • 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.jsonc ratelimits array, but remember the namespace IDs are shared with agentCommunity_PAGE — coordinate with that repo before changing values.
  • Duplicate certsalready_recorded=true means 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_requested remains non-unique.

Common issues

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

6. Future — Not Yet Implemented

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: a card_views table (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-agent to 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 200 issued/not_found union.
  • Badge by cert IDbadge edge 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 bunx for cross-language support
  • Admin dashboard — view registrations, manage verifications, handle disputes

Architecture Reference

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.