Skip to main content
This is not where a new parent application starts. New provider integrations begin at the Provider QuickstartcreateGoldenLinkServer({ credential, database, authenticate, children, policy }) plus the SDK-owned PhosraLink component. That is the recommended durability boundary, and it is what the reference provider integration runs.This page documents the writer-plane compatibility lane: createLink(...) and the ceremony primitives underneath it. Both lanes are live exports of the same package, so picking the wrong one does not error — you just build the one you did not mean to.
Which lane am I on? Look at your secret.Credentials are issued by Phosra through the operator workflow, not minted by you — see the issuance note on the Provider Quickstart. If you are still waiting on one, that is the reason to read this page; it is not a reason to ship on this lane permanently.
createLink() is the ergonomic front door to the writer plane of @phosra/link. It derives the whole hand-built LinkConfig (writer DID, router DID, router payload key, the HKDF-derived household and parent keys) from one Ed25519 writer key you already hold, ships and migrates its own product tables, and delivers rules to platforms by signing each delivery to the OCSS root — so there is no shared HMAC secret to mint, hand off, store, sync, or rotate.
The lower-level primitives createLink wraps — createLinkSession, completeLink, runConnectCeremony, directive, provisionProfiles, deliverLabelToPlatform — are unchanged and still exported; see the @phosra/link reference.

You will likely run both lanes

The two lanes are not mutually exclusive, and the reference provider integration uses both in one application — nothing here is a fork in the road you can never come back from:
  • createGoldenLinkServer owns the connect ceremony, the durable worker, the PhosraLink component, and the evidence-backed session status. It is the durability boundary.
  • createLink / makeLinkStore stay useful for the product-side reads and writes around it — listing a child’s grants (listGrantsByChild), revoking one, and signing direct census calls with the writer key from link.config.
Both read the same Postgres. Run the Golden server for the ceremony first, and reach for the writer plane only for the product surfaces the Golden server does not expose.

Install

@phosra/link is ESM-only. Import it from an ESM module ("type": "module", a .mjs file, or TypeScript compiled to ESM). From CommonJS, load it with a dynamic await import('@phosra/link') — not require.

Integrate in ~15 lines

That is the entire writer-plane setup. writerSeed is the only secret, and it is shared with no platform — the same key signs your census writes and your platform deliveries.

What each field does

writerKeyId is the one field the blueprint’s 3-field pitch elides. createLink needs your key id to sign — did:ocss:<slug>#<kid> — unless you pass writerSeed as a full SenderKey { seed, keyID }, in which case it is read from there. For a self-registered DID the <kid> is the YYYY-MM the census assigned at registration — read it back, don’t invent one.
Trust root: pinned vs. explicit. For a census in PINNED_TRUST_ROOTS (resolvePinnedTrustRoot(census) returns it), the SDK verifies against the bundled pin and you pass nothing. For any other host — a self-hosted or one-off census — pass trustRoot explicitly. Root verification exists precisely so you don’t fetch the root from the census you’re about to verify.
link.ready() runs ensureSchema (the idempotent product DDL) and verifyAccreditation (root-verify + writer-pub match + active enforcement-agent role). Prefer to control timing? Call link.migrate() and link.verifyAccreditation() yourself.
Phosra-issued production credentials publish this standing for you. Issuance writes the credential’s active Link public key and the required enforcement-agent role to the Trust List source in one transaction. You should not add a separate role-setting deployment step. If the existing DID already carries a different role, issuance fails closed instead of overwriting it.

The connect flow

The connect flow links a parent’s account on a platform to a child’s OCSS policy. It is three legs on the link.connect sub-object. The parent authenticates with your auth — Phosra never sees parent credentials; parentSessionRef is the server-derived binding between that login and the ceremony (never accept it from the client).
connect.finish does everything the old four-step ceremony did — derives the unlinkable household hash, mints the grant, ingests the §8.3.2 parent-consent attestation, binds the enforcement endpoint, and delivers the label to the platform, signed to the OCSS root.
deliveryScheme tells you which lane carried the label. On a secret-free platform it is "ed25519-did" (signed). It is "hmac" only if you passed a legacy __legacyConnectSecret during a migration drain window — see Migrating HMAC → Signed. delivered + deliveryStatus report the receiver’s HTTP result.
Two connect vocabularies, one flow. The factory’s legs are start / resume / finish; the embeddable @phosra/connect transport names the same three legs init / complete / bind. They map 1:1 — startinit, resumecomplete, finishbind — so your BFF wraps the factory to serve the modal.

When the platform advertises batch provisioning, a single signed call creates the child profiles on the platform and binds them — the create-and-link path (ingestConsentAttestationmintEnforcementEndpoint(standingRef) → signed provisionProfiles, all wrapped):
Every profile is created under one signed, audience-bound delivery to the platform — no shared secret, no per-child round trip. The platform side is a single receiver route; see Platform · createConnectReceiver (legacy connect receiver).
Once a grant is active, link.enforce signs and posts the rule write to the census.
The write is scope-checked before signing: a category outside the grant’s granted_scope throws before any network call. The standing (consent:attestation:<ref>) is supplied automatically from the grant.

Manage grants


Typed errors

@phosra/link throws typed LinkError subclasses so you can turn a raw census status into an actionable message. Guard with isLinkError(e) and branch on e.code.
A DeliveryFailedError with .status === 401 almost always means the platform has not yet added your provider DID to its createConnectReceiver({ authorize }) allowlist — the one trust config on the platform side. That is the signed-delivery analogue of “wrong HMAC secret,” and the fix is a one-line allowlist edit on the platform, not a secret exchange.

What the SDK owns (and never sends)

Signing and verification are local; billing is enforced server-side against your org, never inside the package and never on the safety path.

What the platform (your partner) must do

The signed model shifts almost all of the work to your side. To receive your deliveries, the platform implements one route and adds one allowlist line — no shared secret:
  1. Install @phosra/gatekeeper and add POST /api/ocss/connect via createConnectReceiver({ env, did, seed, authorize: ["did:ocss:<you>"], store, onBound }) — see Platform · createConnectReceiver. (A platform on the Golden Platform Quickstart instead receives deliveries at its facade-owned /api/phosra/delivery route — no receiver to add.)
  2. Put your provider DID in that authorize allowlist. That is the entire trust configuration; there is nothing to mint, store, sync, or rotate.
  3. Nothing else. The platform never holds a Phosra secret; your signature to the pinned root is the auth.

Next