Conventions & contract
Why the endpoints all look the same: the auth model, the response
envelope, the error model, and the 407 reauthentication
pattern.
The auth model
Auth is enforced per route, in the Express app — the function itself is publicly invokable. Three tiers:
-
Firebase ID token — every business endpoint reads
Authorization: Bearer <token>and verifies it with the Firebase Admin SDK. The decoded token identifies the caller; authority always comes from the token, never from the request body — clients never pass their own user id. -
Admin claim — the
/v1/admin/*subtree additionally requires theadmin: truecustom claim; a valid token without it gets a403. - Provider shared secret — webhooks (e.g. RevenueCat) are mounted outside the Firebase auth chain and validate their own shared secret with a constant-time comparison.
The API never mints or issues credentials of its own — Firebase Auth stays the identity provider, and there are no API keys.
The response envelope
Every /v1 business endpoint returns the
same envelope, so the Kiroku app's existing response
middleware can apply it unchanged. This is non-negotiable — it is the
reason the API exists.
{
"jsonCode": 200, // success switch; HTTP status is mirrored into it
"onyxData": [ // OnyxUpdate[] — applied via Onyx.update(onyxData)
{ "onyxMethod": "merge" | "set" | "mergecollection" | "clear", "key": "<ONYXKEY>", "value": <any> }
],
"lastUpdateID": 0, // optional, incremental-sync ordering
"previousUpdateID": 0, // optional
"requestID": "..." // optional
}
-
onyxDatais the payload. The client feeds it straight toOnyx.update(onyxData). Success means HTTP200with anonyxDataarray (possibly empty for a no-op); failure means a non-200jsonCodewith an empty or absentonyxData. -
mergedeletes vianull. To remove a nested child, the server emits it with valuenull(Onyx semantics) rather thanset-ing a whole subtree. -
mergevssetmatters. For example/v1/app/openreturnsmerge session, notset— asetwould drop the token fields the client already holds. -
lastUpdateIDorders incremental updates; bootstrap calls ("bring me current") may omit it.
The error model
-
401Unauthorized — missing, malformed, or revoked Firebase ID token (an expired token is different — see below). -
403Forbidden — valid token but missing theadminclaim on an admin route. -
400Bad Request — failed input validation. Business routes return the contract body{ jsonCode: 400, onyxData: [], message }; validation is strict, at the route, before any I/O. -
500Internal error — anything thrown. Business routes return a generic{ jsonCode: 500, onyxData: [], message: "Internal error" }so internals never leak. -
Admin routes use a separate body shape,
{ error: string, code?: string }—codeis a stable identifier (e.g.USER_NOT_FOUND) client code branches on. -
Webhooks validate a shared secret and intentionally
ACK
200even on a write failure — the writes are idempotent state transitions keyed by event id, so a logged failure is more useful than a provider retry storm.
A 200 with empty onyxData ("no-op success")
is deliberate and appears in several places — the operation succeeded,
there is simply nothing for the client to merge.
Reauthentication — the 407 pattern
When the Firebase ID token has expired (as opposed to
being missing or invalid), the API responds with HTTP
200 and the envelope:
{ "jsonCode": 407, "onyxData": [] }
The 407-inside-HTTP-200 is deliberate: the
client treats any non-2xx HTTP status as a network error and would
never inspect jsonCode. By answering 200,
the client's reauthentication middleware sees the 407,
refreshes the Firebase ID token, and replays the request. Missing,
malformed, or revoked tokens stay a plain HTTP 401 — a
refresh would not help there.
See also
- Reference — the endpoint inventory.
- Getting started — see the envelope on the wire.
Kiroku API