API-key minting (local e2e)
Scope: getting a raw opl_sdk_… / opl_api_… key against the local
backend so SDK live-e2e suites can run. Does not cover SDK-side test commands —
those live in each SDK repo's docs/test.md.
Recipe verified as recorded (legacy TESTS-NOTES: node 2026-09-20, android 2026-09-23, first-run green). Re-verify on next use.
1. Unlock api_access on the demo org
/portal/api-keys returns {locked:true} when the org's plan lacks api_access
(SOLO and below). The seeded Demo Org is starter → locked. Fast unlock (the
seed resets it):
psql "$DATABASE_URL" -tAc "UPDATE \"Organization\" SET plan='growth' WHERE \"clerkOrgId\"='org_3GX2Hsem68kleDYAPyaqPfmMqrQ';"
2. Mint an org-active token (Backend API directly)
The Clerk CLI POST /sessions 404s — use the Backend API:
- Demo user id from the DB (the column is
clerkUserId—"clerkId"does not exist):SELECT "clerkUserId" FROM "User" WHERE email='demo@optolink.io'; POST /v1/sessions, thenPOST /v1/sessions/:id/tokenswithorganization_id=org_3GX2Hsem68kleDYAPyaqPfmMqrQ.- Token lives ~60s — mint and use immediately.
3. Regenerate the key — raw key ONLY in the response
POST /portal/api-keys/SERVER/regenerate (or /CLIENT/regenerate) with that
(token) — capture the raw keys for the checks below:
SRV=$(curl -s -X POST localhost:3000/portal/api-keys/SERVER/regenerate \
-H "Authorization: Bearer $TOK" | node -pe 'JSON.parse(require("fs").readFileSync(0)).rawKey')
SDK=$(curl -s -X POST localhost:3000/portal/api-keys/CLIENT/regenerate \
-H "Authorization: Bearer $TOK" | node -pe 'JSON.parse(require("fs").readFileSync(0)).rawKey')
The raw key appears only in the response's .rawKey field;
GET /portal/api-keys shows masked displayKey forever after.
4. Prove the keys (tier enforcement)
curl -s localhost:3000/portal/api-keys -H "Authorization: Bearer $TOK"
# → {sdkKey:{prefix:"opl_sdk_",displayKey:…},serverKey:{…},locked:false}
# (plan without api_access → {sdkKey:null,serverKey:null,locked:true})
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:3000/sdk/session \
-H "Authorization: Bearer $SDK" -H 'Content-Type: application/json' \
-d '{"deviceId":"rt-1"}' # → 200 (CLIENT key on CLIENT surface)
curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/links \
-H "Authorization: Bearer $SRV" # → 200 (SERVER key on link CRUD)
curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/links \
-H "Authorization: Bearer $SDK" # → 403 (valid key, wrong tier)
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:3000/portal/api-keys/NOPE/regenerate \
-H "Authorization: Bearer $TOK" # → 400 (unknown tier; consumes a regen point)
Design detail for these endpoints (grace window, rotation, D-1 retrievability): Authentication architecture.
Gotchas
POST /linksreturns the raw row +urlbut NOdomainobject; only list/get carrydomain; PATCH returns a bare row.- The
/links429 preset (100 req/s) fires no rate-limit headers — to trip it, burst in parallel (~140 parallel PATCHes yields ~12×429); a sequential loop never trips it.