Skip to main content

When everything returns 503

This page is about createPlatform from @phosra/gatekeeper — the catch-all platform facade that mounts /api/phosra/{status,par,authorize,token,delivery,retry}. Everything below (adapter.authorizedProviders, adapter.database, GET /api/phosra/status, ready()) is that factory’s surface. The SDK reference is @phosra/gatekeepercreatePlatform.It does not apply to createConnectReceiver from @phosra/gatekeeper/next, which returns an object with exactly one member — POST. That factory mounts no status route, exposes no ready(), and has no readiness machinery at all. If you followed the createConnectReceiver quickstart and are curling /api/phosra/status, you will get your framework’s HTML 404 no matter which SDK version you install. Move to createPlatform before using this page.
Your /api/phosra/authorize returns this and nothing else:
That message is identical whether your credential is missing, your standing lapsed, your database never migrated, or your signing key diverged from the census. It is deliberately opaque — it is served to the public internet — and it is undiagnosable from outside. GET /api/phosra/status is the answer. Same deployment, same process, same request cycle. It always answers 200 and names the outstanding precondition.
Requires @phosra/gatekeeper ≥ 0.8.68. Published and live on npm. If you declare ^0.6.0, npm will not resolve 0.8.68 — a caret on a 0.x version pins the minor. Change the range to ^0.8.68.If you vendored the package, remove the vendored copy first; a vendored build will not have this route no matter what your package.json says.
No code changes. The route is mounted by the same phosra.next.handlers() that already mounts your protocol routes, and is also reachable directly at platform.handlers.status.GET.

Read it

Real output from a deployment whose credential is not set (captured from published 0.8.68):
In that same process, at that same moment, /api/phosra/authorize returns 503. That is the point: the diagnostic is not behind the gate it explains. A diagnostic that 503s under the fault it describes is not a diagnostic.

Fields

Absent fields are absent facts. No signingKey means the check never got that far, not that the key is fine.

?probe=0

Reads current state without triggering a bootstrap attempt. Use it when you are polling, or when you want to know whether anything has attempted bootstrap at all — PLATFORM_GENERATION_INACTIVE is only observable this way. Without probe=0 the endpoint reuses the existing generation coordinator: a healthy platform answers with zero network I/O, and an unhealthy one respects the coordinator’s bounded exponential backoff (30s base, 5min cap). You cannot hammer the census with this endpoint. Non-GET methods return 405 with Allow: GET.

Is it safe to expose?

Yes, and the reasoning is worth stating rather than assuming. Every field is already public: DIDs are public identifiers; the trust list is world-readable at /.well-known/ocss/trust-list; a signing key’s public half is precisely the thing meant to be published; missing is the SDK’s own schema object names; versions are in your package.json. Gating it would require distributing a new shared secret — the exact friction Phosra Link exists to remove (OCSS §8.1.6 bans bearer secrets) — and would withhold the answer from the person most likely to need it: a partner debugging your deployment from outside it.
Secret containment is structural, not hopeful. A single audited funnel converts credential key material into the report. It reads the public .x member itself, so no caller can pass a seed under the name x, and it refuses outright if the bytes it is about to publish are byte-identical to the private seed. The endpoint cannot emit your seed, your private key, or your credential bytes.

The thirteen preconditions

Summary. Every Phosra precondition passed. The protocol routes are serving.Do. Nothing. If a protocol route is still failing, the fault is downstream of readiness — read the correlationId in the route’s own error body.
PLATFORM_READY is not “the integration works.” Three things this endpoint structurally cannot see, all of which break a ceremony after boot:
  • Your profiles_url may be a 404. The directory check verifies the URL’s origin, never that it answers — and the catch-all does not serve a profiles route, you do. A live production platform is reporting PLATFORM_READY today with a profiles_url that returns 404 PHOSRA_ROUTE_NOT_FOUND. Curl yours.
  • Your worker may not be running. Readiness reports the HTTP process. Delivery happens only in the worker pass, so a platform with no worker looks ready and never finishes a connection.
  • Your DID may not be on the census’s consent-attestation roster, in which case the provider’s consent attestation naming your app is refused 403 failed binding: app_ref and no ceremony ever reaches you. That is a census-side operator setting — ask Phosra.
Summary. No Phosra credential is configured. The value handed to createPlatform is not a Phosra credential envelope.Cause. PHOSRA_CREDENTIAL is unset, empty, or holds something that is not an envelope (envelopes start phosra_cred_v<n>.). A very common shape: the variable exists in your dashboard but was never rebuilt into the image.Do. Set it to the credential value you were issued and redeploy. Confirm the deploy actually rebuilt — versions in this same response tells you which SDK is running.
Where a platform credential comes from, honestly. The remedy string above names POST /developers/orgs/{orgId}/apps, and that funnel is real — but it is live only on the Phosra-internal staging sandbox census, and a freshly self-registered DID is provisional while issuance requires accredited. Today a platform credential is operator-issued: email developers@phosra.com with your DID (or desired display name), your canonical application origin, and sandbox-or-production. See Getting a platform credential.
Summary. A Phosra credential envelope is configured but could not be parsed or bound to this environment.Causes, in the order worth checking.
  1. Truncated on copy — envelopes are long and survive no line-wrapping.
  2. Environment mismatch — a sandbox credential in a production deployment, or vice versa.
  3. Manifest drift. The credential pins the environment manifest’s id and a config_digest computed over the manifest’s whole configuration object. If any part of that configuration changes — sdk_minimum, origins, rule-registry edition, root key id — every previously issued credential fails to bind at once. This is a global condition, not a per-deployment one: if several unrelated platforms broke simultaneously, look here first.
Do. Re-copy whole. If (3), the credential must be re-minted against the current manifest — an SDK upgrade will not help.
Summary. The signed Phosra environment manifest could not be fetched or verified against the credential’s pins.Cause. Egress blocked to the management origin, DNS failure, or a credential whose pinned manifest signer does not match what that origin serves.Do. Confirm this deployment can reach the management origin over HTTPS, and that the credential’s environment matches the deployment.The manifest is not JSON, and not a bare compact JWS: it is a compact JWS behind a phosra_env_v1. envelope prefix, so it splits into four dot-separated parts, not three. A standard JWS decoder — or cut -d. -f2 — decodes the wrong segment and hands you a base64 error. The payload is part 3:
That one command also hands you the two values the other accordions need: the census_origin your credential is bound to, and the operating_router_did whose standing gates your boot.
expires_at is real. The manifest is re-signed periodically; a deployment that cached a verified environment and then lost egress will eventually fail closed here rather than serve on stale trust.
Summary. The census trust list could not be fetched or verified to the pinned OCSS root.Cause. Census unreachable, or the credential pins a root this census does not sign with. The second is the nastier one and looks like a network problem.Do. Fetch the list yourself and compare the root against what your credential pins:
Production’s root public X is 824vsCATBxyUiA-znpGx01N48NNs_3gPE3M7f7vIEaI. Pin the public X, not the key id. A credential pinning the retired root-prod-2026-06 fails closed on every current document — correctly.
Summary. The census publishes no signing key under this platform’s active key id.Cause. Your credential names a kid that is simply not on your trust-list entry — often after a rotation that added a new kid and dropped the old one, while a deployment still holds a credential naming the dropped one.Do. Read signingKey.kid from the response, then list what the census actually publishes:
document is served as a JSON string, not a nested object — it is the exact byte sequence the root signature covers. Hence the fromjson. Parsing it any other way changes the bytes and breaks verification.
Real output for a live entry:
Publish that kid on the entry, or re-provision so credential and trust list come from one key.
Summary. The credential’s active signing key is not the key the census publishes under that key id. The credential and the trust list have diverged.This is the failure class. The response prints both public halves side by side:
An SDK upgrade cannot fix this. Both sides of the comparison are data. The credential embeds one key; the census publishes another; nothing structurally forced them to agree, because they were minted separately.Do. Re-provision through /apps, which generates the keypair and mints the credential from that same key, so they cannot disagree. Then set the returned credential and redeploy. See the divergence note for how this happens and who is exposed.
Summary. This platform’s trust-list standing is not active and accredited.Cause. Most often a conformance attestation that passed its TTL and needs re-issuing. Also: the entry was revoked, suspended, or valid_through elapsed.Do. Read the standing object in the response, then confirm against the census:
You want active and accredited. Restoring standing is an operator action — attestations are issued by a verifying agency, not self-served.
Because a live recorder survives a successful boot, this code also catches a mid-life divergence — a rotation or an attestation lapse that happened after your app booted — not only a boot-time one. A later success clears it rather than reporting it forever.
Summary. A configured counterparty is not active + accredited with the required role on the census.Your credential is fine. You are failing on someone else’s trust-list entry. There are two distinct counterparties behind this one code, and only one of them is yours.Cause 1 — a DID in adapter.authorizedProviders. Every entry must be status: active, tier: accredited and role: enforcement-agent.
The most common real cause is a conformance attestation that passed its TTL on a partner’s entry, which silently downgrades accredited to verified.Cause 2 — the census’s operating router, which you never configured. createPlatform resolves the census’s operating router from the signed environment manifest (configuration.routing.operating_router_diddid:ocss:phosra-router on every current census) and hard-fails boot if that entry is not active + accredited. The router is the signer whose signature is on every enforcement profile you consume, so if it lapses nothing can be trusted and nothing boots.
The router carries no role — only status and tier are read for it. Do not apply the enforcement-agent check to the router; it will look broken when it is fine.And if the router is the failing party, that is not a fault you can fix. You do not configure its DID, its standing, or its attestation. Report it with the versions block from this response rather than changing your own configuration.
Summary. This platform’s own directory presentation could not be resolved.Before it boots, the SDK fetches its own row from the census and compares it to your credential on six values, five of them byte-for-byte. Any one of them off by a character is this code, and every /api/phosra/* route then serves an opaque 503 — with a correct credential, correct standing and a correct origin.Do. Read the row and check every field, not just one:
Three causes, in the order worth checking:
  1. Origin drift — the application origin in the credential is not where the app is actually served: a preview URL, a new custom domain, a moved Railway/Vercel host. The credential must be re-issued for the new origin and the row re-published; moving one alone leaves you permanently 503.
  2. Display-name driftname differs from the credential’s display name. This is invisible in every other check and fatal here.
  3. An unexpected member in the row. The row is a closed set: beyond the six required fields only scopes, icon_url, connect_url, provisioning_form and profile_management_url are permitted. An extra member fails the whole row rather than being ignored.
A 404 provider connect config not available means no directory entry exists for that DID at all.Publishing or correcting the row is PATCH /api/v1/platforms/{did}/connect — RFC-9421 signed, self-scoped, JSON-merge semantics (null deletes a member), never rotates labels or secrets. See the directory-row section of the gatekeeper reference. At onboarding an operator seeds it for you to match the credential they issue.
Summary. Trust and directory verification passed, but platform composition failed.Do. This one is ours, not yours. Report it with the correlation id from the onDiagnostic hook, plus the versions block from this response.
You only have a correlation id if you wired the hook. onDiagnostic is a first-class option on createPlatform and is absent from most quickstart snippets:
The payload is content-free. It is also the only channel for two whole failure classes that never reach this endpoint at all — operation: "materialization" (a delivery pass failed mid-flight) and operation: "target_aggregate_lifecycle". See onDiagnostic.
Summary. The package-owned schema did not migrate or did not report ready.Cause. Entirely your side: unreachable Postgres, or a role that cannot create the objects the package owns.Do. Read database.missing — it lists the exact object names still absent. Confirm the Pool handed to adapter.database points at a reachable Postgres the app is allowed to migrate, then retry.
Summary. No verified platform generation is active and no bootstrap attempt has been recorded yet.Cause. Either you passed ?probe=0 before anything ran, or the process genuinely has not attempted bootstrap.Do. Re-read without probe=0 to trigger one bounded attempt, then read the real code.

A 2am triage order

1

Confirm the endpoint even exists

curl -s .../api/phosra/status, and read the shape of the failure — the three cases have three different fixes:
  • 404 with {"error":{"code":"PHOSRA_ROUTE_NOT_FOUND"}} — the SDK is mounted and answering, but predates 0.8.68. Upgrade.
  • Your framework’s HTML 404 — nothing Phosra is mounted at that path. Either the catch-all is not wired, or you built with the wrong factory: createConnectReceiver mounts a single POST and no status route, so no version of the SDK will ever answer here. Check which factory your singleton calls before you touch package.json.
  • 307/302 to a login page — your own auth middleware is shadowing /api/phosra/*. Exempt that prefix.
2

Read `precondition`, not the logs

It names the outstanding condition directly. remedy names the fix.
3

Check `versions`

Confirms which SDK is actually running — the fastest way to catch a deploy that did not rebuild.
4

If it is a key or standing fault, verify against the census yourself

The trust list is public. Compare signingKey.censusPublicKey with what /.well-known/ocss/trust-list publishes for your DID.
5

If it is a divergence, re-provision — do not retry

PLATFORM_SIGNING_KEY_MISMATCH never heals on its own. See the divergence note.

What readiness cannot check

Readiness proves the boot preconditions. Four things break a live ceremony and are invisible here, so check them by hand once per deployment:
The fifth is not yours to check: your DID must be on the census’s consent-attestation roster before a provider can land the attestation that starts a ceremony against your platform. If your platform is PLATFORM_READY and nothing ever arrives, ask Phosra to confirm it.

@phosra/gatekeeper — createPlatform

The full SDK reference: the directory row, the profiles endpoint, authorizedProviders, framework-agnostic mounting, the worker, and how to obtain a credential.