Phosra ships changes additively. A stable v1 surface — an endpoint, a response
field, an SDK export — never disappears or changes shape under you without a named
deprecation, an advance-notice window, and a machine-readable signal on the wire. This
page is that contract: what "deprecated" means, how long you get, what you will see,
and how to migrate.
Verified live. The endpoint behavior and header presence on this page were
captured from https://phosra-api-sandbox-production.up.railway.app on 2026-07-06.
For how the four version layers pin together, start with Versioning.
The one guarantee
We do not make silent, breaking, immediate changes to a stable v1 surface.
Every breaking change is announced first, carries a machine-readable deprecation
signal, and honors a minimum notice window before the old behavior can be removed.
Everything below is the mechanism behind that sentence.
Lifecycle stages
Every stable surface moves through four stages, in order. It can sit in a stage
indefinitely, but it can never skip one.
Stage
What it means
What your code should do
Active
Fully supported. The default state of everything in v1.
Nothing.
Deprecated
Still works exactly as before, but a replacement exists and a removal date may be set. Marked in the changelog, in the SDK (@deprecated), and — once it enters a sunset window — on the wire via a Deprecation header.
Plan your migration. No rush, but stop building new usage on it.
Sunset
A removal date is now committed and published in a Sunset header. The surface still responds until that date.
Migrate before the Sunset date.
Removed
Gone. The endpoint returns 404; the field is absent; the SDK export no longer exists in that major line.
Already migrated.
"Deprecated" is not "broken." A deprecated endpoint returns the same status
codes and the same body it always did. The only thing that changes is that we have
told you a better path exists and, eventually, when the old one stops. You are never
forced to move on the day of the announcement.
Advance-notice windows
The minimum notice you are guaranteed depends on the blast radius of the change. These
are floors, not targets — most changes get more.
Change class
Example
Minimum notice before removal
Additive (non-breaking)
New endpoint, new optional request field, new response field or enum value
A route superseded by a better-named one; a response field renamed with both served during overlap
≥ 90 days from the Sunset header / changelog entry to removal
New major API version (/api/v2)
A genuinely incompatible contract
A new path. v1 keeps serving; ≥ 12 months overlap before a v1 sunset can even be proposed, and that proposal is its own separately-announced deprecation
Security-forced break
A change we must make to close a vulnerability
As much notice as is safe to give, clearly labeled security. This is the only lane that can be shorter than 90 days, and only when a longer window would leave users exposed.
Pre-1.0 SDKs are the exception you opt into. The published packages are 0.x
(@phosra/[email protected], @phosra/[email protected], @openchildsafety/[email protected], …). Under
semver, a 0.xminor bump may carry a breaking change with no separate notice
window — that is what 0.xmeans. Pin exact and read the changelog before you
bump. The wire API (v1) gives you the windows above; a floating SDK caret range
does not. See SDK versioning.
On the wire: Deprecation and Sunset headers
Phosra follows the two IETF standards for signalling this in HTTP, so a generic client
or gateway can detect a deprecation without reading these docs:
Deprecation — RFC 9745. A
Structured-Field Date (@ + Unix seconds) marking when the resource became (or
will become) deprecated. It may be a future date.
Sunset — RFC 8594. An HTTP-date
marking the moment the resource stops responding.
Link — a pointer to the human migration guide, related by
rel="deprecation" and/or rel="sunset".
A response from an endpoint that has entered a sunset window looks like this:
Read it as: "this became deprecated on 2026-09-01 (@1788220800), it stops responding
after 2026-12-01, and the migration guide is at the linked URL." The 200 and the body
are exactly what you got before — the headers are the only difference.
Deprecation carries a Structured-Field Date, so the value is @ followed by
Unix seconds — not an HTTP-date. Sunset carries a classic HTTP-date. They are
intentionally different formats because they come from two different RFCs; parse each
accordingly.
What you see today
No v1 endpoint has entered a sunset window yet, so you will not see a Sunset
header on a live response today — and we would rather you verify that than take our
word for it:
bash
BASE=https://phosra-api-sandbox-production.up.railway.appcurl -s -D - -o /dev/null "$BASE/api/v1/platforms" \ | tr -d '\r' | grep -iE '^(deprecation|sunset|link):' \ || echo "no deprecation / sunset / link headers — nothing in a sunset window"# → no deprecation / sunset / link headers — nothing in a sunset window
The block above is the exact shape you will parse when a surface does enter its
window. Wiring the check now means your integration lights up the day one appears,
instead of the day it breaks.
The one deprecation live today
There is exactly one deprecated route on the current census: the legacy advisor
self-registration shim.
Deprecated
Replacement
Route
POST /api/v1/advisors/register
POST /api/v1/advisors/self-register
Status
Still serving — forwards to the replacement, adds no new access
Canonical route
Why
The old route multiplexed two different call shapes on one path; the split gives each its own single-contract endpoint
—
It still works — it forwards by the did discriminator and returns the same result the
replacement would:
bash
BASE=https://phosra-api-sandbox-production.up.railway.app# A 32-byte-key validation error proves the request reached the handler and forwarded —# the route is live, not 404. (Send a real key to get a 200 provisional registration.)curl -s -X POST "$BASE/api/v1/advisors/register" \ -H "Content-Type: application/json" \ -d '{"did":"did:ocss:example","public_key_b64url":"AAAA","roles":["editor"]}'# → {"error":"Bad Request","message":"invalid_public_key_b64url: must decode to 32 bytes, got 3","code":400}
Migration is a one-line URL change — the request and response shapes are identical:
The published @phosra/cli already calls self-register, so phosra register needs no
change — only hand-rolled callers of the old path do. See the
CLI reference and self-serve registration.
How a deprecation reaches you
We announce on every channel a developer actually watches — no single point of failure:
Deprecation + Sunset + Link on the wire, so automated monitoring catches it
without reading docs.
Your account email
Owners of orgs with recent traffic on a deprecated surface get a direct notice at
announcement and before the sunset date.
The SDK
Deprecated exports carry a @deprecated JSDoc tag (your editor and tsc flag it)
and a 0.x → next-minor removal follows semver. Read the changelog before bumping.
Detect deprecations programmatically
Fail loud in CI or a health check the moment any endpoint you depend on starts
advertising a sunset. This runs today (and prints nothing, correctly):
bash
BASE=https://phosra-api-sandbox-production.up.railway.appfor path in /api/v1/platforms /.well-known/ocss/trust-list; do hdr=$(curl -s -D - -o /dev/null "$BASE$path" \ | tr -d '\r' | grep -iE '^(deprecation|sunset):') if [ -n "$hdr" ]; then echo "DEPRECATION NOTICE on $path:"; echo "$hdr" fidoneecho "checked — no sunset windows open"
What we will never do
Break v1 in place
A breaking change gets a new path (/api/v2), never a mutation of v1.
Remove without a Sunset
Nothing in v1 is removed until a Sunset date has been published and the notice
window has elapsed.
Change a field's meaning
We add a new field beside the old one; we do not silently repurpose an existing
field or enum value.
Substitute frozen artifacts
A pinned Annex B edition is content-addressed — its bytes never change, and a
missing edition is a clean 404, never a nearby substitution. See
Versioning.
Migration expectations
When you receive a deprecation notice, the contract you can count on:
A named replacement exists before the notice ships. We never deprecate a path
without a place to go.
The old and new surfaces overlap for at least the notice window — you can run
both, cut over, and verify without a flag-day.
The migration is documented at the Link: rel="deprecation" URL, with a
before/after example like the one above.
Idempotent, replay-safe cutover. Re-issuing the same write against the new path
is safe — see Idempotency.