Skip to main content
Run this the day you flip from the sandbox to production. It is a short, mechanical pre-flight — eight checks, each with a copy-paste command and a link to the page that owns the detail. Nothing here is aspirational: every command below was run live against the sandbox census.
This is the engineering pre-flight — not accreditation. Getting your entry onto the production Trust List is a separate governance gate with its own vetting, SLA, and eligibility bar. Do that on Production Accreditation. Come back here for the code-side switch-over once you are cleared.

The checklist

  • 1. Rotate test → live keys — mint phosra_live_ keys; retire every phosra_test_ key from prod config
  • 2. Lock scopes to least privilege — each key holds only the scopes its service calls
  • 3. Verify webhook signatures in prod — reject any delivery whose HMAC does not recompute
  • 4. Arm Deprecation / Sunset monitoring — alert on any Deprecation or Sunset response header
  • 5. Diff sandbox → prod config — base URL, trust root, and org id all move together
  • 6. Confirm rate-limit headroom — read X-RateLimit-Remaining; back off on 429, never hammer
  • 7. Pin exact SDK versions — no caret ranges on a pre-1.0 line
  • 8. Wire health / status monitoring — poll /health and the trust-list ETag
1

Rotate test → live keys

A phosra_test_ key only works against the sandbox base URL; a phosra_live_ key only works against production. Mint live keys, load them into your prod secret manager, and make sure no phosra_test_ value survives in production config.
Mint a live key
curl -X POST https://prodapi.phosra.com/api/v1/developers/orgs/{orgId}/keys \
  -H "Authorization: Bearer <workos_access_token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"orders-service","environment":"live","scopes":["read:policies","write:enforcement"]}'
The raw key is in the response key field and is shown exactly once — only a SHA-256 hash is stored. Prefer zero-downtime rotation (mint the new key, deploy, then revoke the old) so nothing breaks mid-cutover.Authentication · test vs live keys
2

Lock scopes to least privilege

Every key carries an explicit scope list; a call to a route your key lacks returns 403, never silent success. Grant the fewest scopes each service needs — a read-only reporting job should never hold write:enforcement. If one key leaks, the blast radius is only that key’s scopes on that one environment.
Read the scopes bound to a key
curl https://prodapi.phosra.com/api/v1/developers/orgs/{orgId}/keys/{keyId} \
  -H "Authorization: Bearer <workos_access_token>" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['scopes'])"
Authentication · scoping
3

Verify webhook signatures in prod

Every delivery is signed t=<unix>,v1=<hex> in the Phosra-Signature header. Recompute HMAC-SHA256(secret, "<t>." + rawBody) over the raw body and reject on any mismatch — otherwise anyone who learns your endpoint URL can forge policy-change events. Use the per-environment signing secret returned when you register the production webhook, and compare in constant time.
Recompute and compare (bash)
# $SIG = the v1=... value from the Phosra-Signature header; $T = the t=... value
EXPECTED=$(printf '%s.%s' "$T" "$(cat body.json)" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary | xxd -p -c256)
[ "$EXPECTED" = "$SIG" ] && echo "verified" || echo "REJECT"
Webhooks · verifying signatures (copy-paste verifiers in TypeScript, Python, and Go)
4

Arm Deprecation / Sunset monitoring

Before any v1 endpoint or field changes, the census sends RFC 9745 Deprecation and RFC 8594 Sunset response headers through the entire advance-notice window. Log or alert on their presence so a migration deadline never reaches you as a surprise 410. Today the sandbox sends none — a clean scan is the healthy baseline you are monitoring for change against.
Scan for lifecycle headers (empty today = healthy)
curl -s -D - -o /dev/null https://prodapi.phosra.com/api/v1/platforms \
  | grep -iE '^(deprecation|sunset|link):' || echo "none in effect"
Deprecation & Sunset Policy · on the wire
5

Diff sandbox → prod config

Three values change together when you leave the sandbox. Miss one and a phosra_live_ key will hit the sandbox census (or vice-versa) and fail closed.
SettingSandboxProduction
API / census base URLhttps://phosra-api-sandbox-production.up.railway.apphttps://prodapi.phosra.com/api/v1
LinkConfig.trustRootXB64Urlsandbox root Xdistinct prod root X (Phosra provides)
LinkConfig.developerOrgIdyour org_… (billing attribution)
API keyphosra_test_…phosra_live_…
Do not copy the sandbox trust root into production — the production root is a different Ed25519 identity (root-prod-2026-06), and pinning the wrong one makes every signature verification fail. Before you pin, confirm the census you are about to trust really is the production one by reading the root key id it signs its Trust List with:
Confirm the production root (run live)
curl -s https://prodapi.phosra.com/.well-known/ocss/trust-list \
  | python3 -c "import sys,json;print('root key_id:', json.load(sys.stdin)['key_id'])"
Output
root key_id: root-prod-2026-06
For an end-to-end round-trip, phosra doctor drives all three values at once. It reads the census from the environment (there is no --env flag) — override PHOSRA_CENSUS_URL and PHOSRA_TRUST_ROOT_X to point it at any census without touching your config file:
Drive the doctor against a chosen census
PHOSRA_CENSUS_URL=https://phosra-api-sandbox-production.up.railway.app \
  PHOSRA_TRUST_ROOT_X=CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU phosra doctor
CLI · phosra doctor · Production Accreditation · after you are accredited
6

Confirm rate-limit headroom

Every metered response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix epoch seconds). Read them instead of hard-coding a quota, and on a 429 sleep until the reset — never retry immediately. Check your standing before a launch that spikes traffic:
Read your current window
curl -s -D - -o /dev/null https://prodapi.phosra.com/api/v1/platforms \
  | grep -i x-ratelimit
Response headers
x-ratelimit-limit: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 1783332360
Rate limits · read your window
7

Pin exact SDK versions

The published SDKs are pre-1.0, so a 0.x minor bump may carry breaking changes. Pin the exact version you tested against — not a caret range — and bump on purpose after reading the changelog. Every version below is live on the npm registry.
package.json
{
  "dependencies": {
    "@phosra/sdk": "0.1.0",
    "@phosra/link": "0.1.2",
    "@phosra/mcp": "0.4.0",
    "@openchildsafety/ocss": "0.1.3"
  }
}
Install pinned
npm install @phosra/sdk@0.1.0 @openchildsafety/ocss@0.1.3
Versioning · pin exact · Forward compatibility
8

Wire health / status monitoring

Poll /health for liveness, and watch the trust-list ETag so you notice when the signed census document (rules, accredited entries, spec version) actually moves — a 304 Not Modified means nothing changed; a 200 means re-read and re-verify.
Liveness + change detection
curl -s -o /dev/null -w "health: %{http_code}\n" \
  https://prodapi.phosra.com/health
# Capture the ETag, then send it back as If-None-Match to get 304 until it moves:
curl -s -D - -o /dev/null https://prodapi.phosra.com/.well-known/ocss/trust-list \
  | grep -i etag
Versioning · conditional reads · Deprecation · detect programmatically

One-command pre-flight

Drop this in your CI or run it by hand right before the switch. It confirms liveness, prints your rate-limit headroom, flags any active deprecation, and echoes the spec version the census is pinned to. Point PHOSRA_API at production once your live keys are loaded.
preflight.sh
#!/usr/bin/env bash
set -euo pipefail
BASE="${PHOSRA_API:-https://phosra-api-sandbox-production.up.railway.app}"

echo "1. Health ......... $(curl -s -o /dev/null -w '%{http_code}' "$BASE/health")"

HDRS=$(curl -s -D - -o /dev/null "$BASE/api/v1/platforms")
LIMIT=$(echo  "$HDRS" | tr -d '\r' | awk 'tolower($1)=="x-ratelimit-limit:"{print $2}')
REMAIN=$(echo "$HDRS" | tr -d '\r' | awk 'tolower($1)=="x-ratelimit-remaining:"{print $2}')
echo "2. Rate headroom .. ${REMAIN:-?}/${LIMIT:-?} remaining"

if echo "$HDRS" | grep -qiE '^(deprecation|sunset):'; then
  echo "3. Deprecation .... ACTIVE — read the Deprecation/Sunset headers"
else
  echo "3. Deprecation .... none in effect"
fi

echo "4. OCSS version ... $(curl -s "$BASE/.well-known/ocss/trust-list" \
  | python3 -c "import sys,json;print(json.loads(json.load(sys.stdin)['document'])['ocss_version'])")"
Output (run live against the sandbox census)
1. Health ......... 200
2. Rate headroom .. 99/100 remaining
3. Deprecation .... none in effect
4. OCSS version ... OCSS-v1.0-pre
All four green, keys rotated, scopes locked, webhook verification live, and monitoring armed? You are clear to flip PHOSRA_API and your keys to production. The request shapes are identical between sandbox and prod — only the base URL, trust root, and key change.

Next steps

Production Accreditation

The governance gate: get your entry onto the production Trust List.

Authentication

Test vs live keys, scoping, and zero-downtime rotation.

Webhooks

Signed event catalog and signature verification in four languages.

Deprecation & Sunset

The advance-notice window and the headers that carry it.

Rate limits

The window headers and a copy-paste backoff loop.

Versioning

Pin the API path, spec version, dated editions, and SDK semver.