Skip to content

Wire Protocol

This document defines the complete client-visible HTTP and JSON contract for Synchro protocol version 3. The terms MUST, MUST NOT, REQUIRED, and MAY are normative.

The five Accepted architecture decisions are locked dependencies of this contract:

  • ADR 001 defines WAL-only pull visibility, commit-safe ordering, opaque row versions, and capture readiness.
  • ADR 002 defines immutable push identity, exact replay, operation shapes, atomic conflict handling, and outcome dispatch.
  • ADR 003 defines incremental pull, opaque cursors, immutable rebuild snapshots, and rebuild replay.
  • ADR 004 defines immutable logical identities, schema actions, client generations, membership generations, and retention.
  • ADR 005 defines canonical portable values, SHA-256 integrity, authorization boundaries, and portable seed continuation.

If an implementation behavior differs from this document or an Accepted ADR, the implementation is nonconforming. Protocol version 2 wire values are not accepted as protocol version 3 values.

Transport, authentication, and adapter boundary

Section titled “Transport, authentication, and adapter boundary”

The four authenticated protocol endpoints are:

Endpoint Purpose
POST /sync/connect Negotiate protocol, client generation, schema action, and scope assignments.
POST /sync/push Submit one immutable batch of client mutations.
POST /sync/pull Read one incremental page across assigned scopes.
POST /sync/rebuild Read one page from an immutable single-scope rebuild snapshot.

The adapter also serves these unauthenticated operator routes:

Endpoint Purpose Exposure rule
GET /ready Return only {"ready":true} or {"ready":false}. A load balancer may reach this route.
GET /sync/schema Return the current schema manifest. Restrict this route to the operator network.
GET /sync/tables Return registered table metadata. Restrict this route to the operator network.

Do not expose internal health, registry, worker, slot, table, or row diagnostics through these routes. The adapter has no TLS listener. Run the standalone server on a private loopback or private network listener behind a reverse proxy that terminates public HTTPS. Do not expose the standalone HTTP listener to the public Internet.

Production requests and responses use HTTPS, UTF-8 JSON, and Content-Type: application/json. Every authenticated protocol request uses Authorization: Bearer <credential>. The credential is opaque to the sync protocol.

The adapter either validates the bearer credential or trusts an upstream component that has already validated it. In both cases, the adapter resolves exactly one canonical authenticated user identity. That identity is transport context and MUST NOT be accepted from a JSON request member. A client_id MUST be bound to that authenticated user.

The adapter is a thin boundary. It authenticates, applies transport and protocol gates, invokes the canonical extension function for the endpoint, and forwards the canonical status and JSON result. It MUST NOT implement mutation conflict rules, scope evaluation, cursor interpretation, checksum calculation, rebuild pagination, or schema lineage decisions. Endpoint work uses one canonical extension call rather than adapter-side database orchestration.

PostgreSQL prints JSONB numbers as numeric text. For example, it prints 1e-7 as 0.0000001 and 1e21 as 1000000000000000000000. The adapter checks each successful connect, pull, rebuild, schema, and tables result. It replaces each number token that differs from the RFC 8785 text of its binary64 value. All other bytes stay unchanged. Content-Length counts the bytes that the adapter sends. The push result is the stored canonical response text, so the adapter returns those bytes unchanged. A result that is not exactly one complete UTF-8 JSON value, or that contains a number outside finite binary64, produces a nonretryable 500 sync_integrity_failure response.

Credentials, cursor values, rebuild continuation values, request bodies, row values, and authenticated identity values MUST NOT be written to operational logs.

Each endpoint request is an exact JSON object. Duplicate member names, unknown envelope members, missing required members, noncanonical UUIDs, wrong envelope or identity types, and values outside declared request limits produce 400 invalid_request, except that a malformed connect schema reference produces 400 invalid_schema_reference. Push field compatibility and value validation use the terminal outcomes defined in the Push section.

An optional member is optional only where this document says it is optional. JSON null does not satisfy an absent-member requirement or a required non-null value.

Every JSON integer in a protocol envelope is in the portable exact range [-9007199254740991, 9007199254740991]. Fields declared nonnegative or positive use 0..9007199254740991 or 1..9007199254740991 respectively. Server-issued schema versions, client generations, scope-set versions, registry generations, membership generations, retention generations, cardinalities, ordinals, and other counters fail before overflow and never wrap, become negative, or leave this range. Opaque token payloads that explicitly define unsigned decimal strings retain that string encoding and their stated u64 range; clients never convert those token internals to JSON numbers.

  • client_id is a nonempty client identifier scoped to the authenticated user. It is not a user identity.
  • client_generation is a positive server-issued integer. Connect returns it. Push, pull, and rebuild require the exact current value.
  • batch_id, mutation_id, and rebuild_id are non-nil UUIDs in canonical lowercase hyphenated form.
  • A retired client_id can never be reused. Requests for it return 409 client_retired.

A normal schema reference has this exact shape:

{
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
}

version is a positive integer. hash is exactly 64 lowercase hexadecimal characters containing the SHA-256 hash of one immutable manifest.

Connect also accepts exactly one fresh-client sentinel:

{
"version": 0,
"hash": ""
}

No other zero version or empty hash is valid. Push, pull, and rebuild never accept the fresh sentinel. They require the exact current schema version and hash. A well-formed but noncurrent reference returns 422 schema_mismatch; a malformed reference returns 400 invalid_request.

Wire member table contains an immutable logical table_id, not a physical relation name. Member names in pk, columns, and row objects are immutable logical field_id values. Human-readable table and field names in a schema manifest are attributes and MUST NOT be used as identity.

Every registered table has exactly one non-null primary-key field. A pk object contains exactly that one field ID. Its portable type is string, int, or int64, and its value uses the canonical representation for that type. Composite, null, decimal, float, temporal, JSON, and bytes primary keys are invalid.

A complete authoritative row contains every synced field exactly once, including the primary-key field. Its primary-key value MUST equal the separate pk value. SQL null is JSON null.

The immutable manifest determines each field’s portable type. Protocol version 3 uses these canonical JSON representations:

Type JSON representation
string JSON string containing valid Unicode scalar values. No normalization is applied.
int JSON integer from -2147483648 through 2147483647, without fraction, exponent, or negative zero.
int64 Canonical base-10 JSON string from -9223372036854775808 through 9223372036854775807.
decimal Canonical exact decimal JSON string without exponent, leading plus, insignificant zeros, or negative zero.
float Finite IEEE 754 binary64 JSON number serialized by the RFC 8785 number rule.
boolean JSON true or false.
datetime UTC JSON string with exactly microsecond precision, such as 2026-07-18T14:00:00.000000Z.
date Valid Gregorian date string in YYYY-MM-DD form.
time Time string with exactly microsecond precision in HH:MM:SS.ffffff form.
json JSON string whose decoded content is one complete RFC 8785 canonical JSON value.
bytes RFC 4648 base64url JSON string without padding.

NaN, infinities, noncanonical integer strings, imprecise decimal conversion, duplicate JSON object members, and values that do not match the manifest type are invalid.

server_version is a server-generated equality token of 1 through 64 octets. Each octet is printable ASCII from 0x20 through 0x7E other than " and \, so its canonical JSON string is its length plus 2 octets. Clients store and return it only as a compare-and-swap precondition. Clients use the length bound only to compute a worst-case request size. Clients and adapters MUST NOT parse, sort, increment, generate, or infer time or stream order from it.

Incremental cursors and rebuild continuation values are opaque server-issued strings. Clients MUST persist and return them unchanged to the endpoint and scope that issued them. Clients MUST NOT decode, compare, edit, synthesize, or derive a row key or checkpoint from them. ADR 003 defines their authenticated server-side bindings and construction. Those internals are not part of client behavior.

Every protocol version 3 row or scope digest uses this exact object with no additional members:

{
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "0011223344556677001122334455667700112233445566770011223344556677"
}

All four members are required. digest is exactly 64 lowercase hexadecimal characters. A different algorithm, version, encoding, shape, length, or alphabet is invalid.

row_checksum is the ADR 005 SHA-256 row digest over the exact schema, logical row identity, complete authoritative row, and accompanying opaque server_version. checksum and values in checksums are ADR 005 SHA-256 scope digests over the exact schema, scope identity, cardinality, and ordered set of row identities paired with row digests. Numeric checksums and serializer-native digests are not protocol version 3 values.

Connect registers or resumes a client, negotiates protocol version 3, returns the current client generation, selects exactly one schema action, and reconciles authoritative scope assignments. It does not return normal row changes or acknowledge incremental cursors. Incremental cursor validation and acknowledgement occur on pull.

{
"client_id": "ios-device-123",
"client_generation": 4,
"platform": "ios",
"app_version": "3.0.1",
"protocol_version": 3,
"schema_reset": false,
"schema": {
"version": 7,
"hash": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
},
"scope_set_version": 12,
"known_scopes": {
"documents_user:u_123": {
"cursor": "opaque-incremental-cursor-a"
},
"templates_public": {
"cursor": "opaque-incremental-cursor-c"
},
"project:p_old": {
"cursor": "opaque-incremental-cursor-b"
}
}
}

The scope IDs in this example are already server-declared and assigned to the authenticated client. A scope name does not grant access or create assignment.

The request members are:

Member Rule
client_id Required nonempty identifier bound to the authenticated user.
client_generation Required positive integer for a continuing local client. It MAY be absent only when the local client is fresh.
platform Required nonempty platform identifier.
app_version Required valid SemVer 2.0.0 string.
protocol_version Required integer 3.
schema_reset Optional boolean. It is true only for an explicit incompatible-schema recovery by a continuing upgraded client and otherwise is absent or false.
schema Required historical or current immutable reference. The exact fresh sentinel is allowed only when client_generation is absent and the server confirms this client identity has no prior generation or installed schema.
scope_set_version Required nonnegative integer for the last locally applied assignment set. A fresh client sends 0.
known_scopes Required map from every locally known scope ID to an object containing cursor, which is an opaque string or null.
seed_receipts Optional nonempty map from seeded scope ID to an opaque portable seed receipt. It is sent only while converting seed continuation state on authenticated connect.

Omitting client_generation asserts a fresh local client. A continuing or restored client MUST present its persisted generation and actual installed schema, including after generation expiry. Pairing a current, stale, or expired generation with the fresh sentinel, or omitting the generation for a previously known client identity, returns 400 invalid_schema_reference without schema dispatch. Connect can replace an expired generation, but it cannot revive a permanently retired client identity.

This example is the response for known Class 3 schema history plus an assignment change:

{
"server_time": "2026-07-18T14:00:00.000000Z",
"protocol_version": 3,
"client_generation": 4,
"scope_set_version": 13,
"schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea",
"action": "rebuild_local"
},
"scopes": {
"add": [
{
"id": "documents_shared",
"cursor": null
}
],
"remove": [
"project:p_old"
]
},
"scope_cursor_updates": {
"documents_user:u_123": null,
"templates_public": "opaque-current-schema-cursor-at-same-position"
},
"schema_definition": {
"schema_version": 8,
"schema_hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea",
"parent_schema": {
"version": 7,
"hash": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
},
"transition_class": "class_3",
"compatibility_floor": 8,
"tables": [
{
"table_id": "tbl_documents",
"relation_id": "rel_documents",
"name": "documents",
"composition": "multi_scope",
"primary_key_field_id": "fld_documents_id",
"lifecycle": {
"created_at_field_id": null,
"updated_at_field_id": "fld_documents_updated_at",
"deleted_at_field_id": "fld_documents_deleted_at"
},
"fields": [
{
"field_id": "fld_documents_deleted_at",
"name": "deleted_at",
"type": "datetime",
"nullable": true,
"writable": false
},
{
"field_id": "fld_documents_id",
"name": "id",
"type": "string",
"nullable": false,
"writable": false
},
{
"field_id": "fld_documents_title",
"name": "title",
"type": "string",
"nullable": false,
"writable": true
},
{
"field_id": "fld_documents_updated_at",
"name": "updated_at",
"type": "datetime",
"nullable": false,
"writable": false
}
],
"indexes": []
}
]
},
"affected_scopes": [
"documents_shared",
"documents_user:u_123"
]
}

server_time is an RFC 3339 UTC diagnostic value. client_generation is the exact positive generation that subsequent first-execution push, pull, and rebuild requests MUST use. scope_set_version and scopes are one authoritative assignment transition. Additions normally have cursor: null and require authoritative materialization. The only initial non-null addition cursor is one issued after successful portable seed receipt validation. Removals contain scope IDs and require local provenance cleanup without creating a mutation. scope_cursor_updates is a required delta map for existing assignments and is present as {} when empty. It never adds or removes a scope.

Protocol compatibility is evaluated before schema lineage. Connect uses exactly this dispatch:

Condition HTTP Action or code
Protocol version or required client runtime is incompatible 426 upgrade_required error
Fresh sentinel is paired with a generation or a previously known client identity 400 invalid_schema_reference
Schema reference is the exact fresh sentinel and the client identity has no prior generation or schema 200 replace
Schema reference is the exact current manifest 200 none
Reference is known compatible Class 2 history 200 replace
Reference is known Class 3 history and affected assigned scopes are nonempty 200 rebuild_local
Reference is known Class 3 history and no assigned scope is affected 200 replace
Reference is unknown lineage or known Class 4 history without explicit reset 200 unsupported
Valid explicit reset with assigned scopes 200 rebuild_local for all assigned scopes
Valid explicit reset with no assigned scope 200 replace

The schema response object always contains the current version, current 64-hex hash, and exactly one action.

Action Required response members Forbidden response members
none Standard connect envelope schema_definition, affected_scopes
replace Current inline schema_definition affected_scopes
rebuild_local Current inline schema_definition and nonempty affected_scopes None of the required members may be omitted.
unsupported Standard connect envelope and schema.reason equal to unknown_schema_lineage or incompatible_schema_transition schema_definition, affected_scopes

affected_scopes is a deduplicated list of assigned scope IDs in unsigned UTF-8 byte order. The client applies the inline manifest and rebuilds exactly those scopes. unsupported is a successful typed connect result, but the client MUST NOT issue normal push, pull, or rebuild requests under that schema lineage.

For a Class 2 or Class 3 action, every existing assignment with a presented historical-schema cursor receives a scope_cursor_updates entry. The server MUST replace it with a current-schema token at the exact same position when the complete known lineage is compatible, every Class 3 transition leaves that scope unaffected, and every other authenticated cursor binding and the resume floor remain current. Only an affected or otherwise stale scope receives null and requires rebuild. Added and removed scopes are excluded from this map. Connect does not acknowledge either the presented or replacement cursor. Pull accepts only the current-schema replacement; the client cannot relabel the old token.

A client in unsupported state can recover only after an explicit application upgrade or operator-approved reset action. It reconnects with its current positive client generation, its actual installed schema reference, and schema_reset: true. The server requires protocol 3 and the current minimum runtime, returns the complete current inline manifest, and either rebuilds all assigned scopes or uses replace when none are assigned. The client preserves local-only data, complete authored queue data, sealed-batch history, and inspectable outcomes. A fresh sentinel with schema_reset: true is invalid.

Protocol version 3 has no fetch schema action and no schema-fetch step in the connect flow.

  • The server is the sole authority for assigned scope IDs.
  • Connect and pull compute the authoritative assignment with the same rule. ADR 004 defines the sources, including the registered assignment function.
  • scope_set_version changes whenever the authoritative assignment set changes.
  • A response with no assignment change has empty scopes.add and scopes.remove arrays. Schema cursor replacement remains a separate scope_cursor_updates delta and does not advance scope_set_version.
  • A new or invalidated assignment has a null cursor and requires rebuild.
  • An expired client reconnect receives a new positive client_generation and null cursors for affected assignments. Its durable mutation queue is preserved.
  • Client-generation expiry does not delete push replay outcomes and does not permit reuse of client_id.

A portable seed receipt is opaque and is not an incremental cursor. The client can return each stored receipt only in seed_receipts on authenticated connect. ADR 005 defines the receipt’s internal bindings and server validation. Clients and adapters MUST NOT decode or edit it.

For an assigned portable scope with a valid receipt under the selected schema and current server state, connect returns a normal user-bound and client-bound incremental cursor in that scope’s scopes.add entry. For an assigned scope with a stale or unverifiable receipt, connect returns the scope with cursor: null and requires rebuild. For an unassigned scope, connect places the scope ID in scopes.remove. A receipt never grants assignment, and it is never sent to pull or rebuild as a cursor.

Push submits one nonempty, ordered, immutable batch. Push operations are exactly insert, update, and delete. upsert is a pull operation and is invalid in a push request. Protocol version 3 has no resurrection operation.

A push request has these limits:

  • The adapter accepts a decoded push JSON body of at most 1,048,576 octets.
  • The extension accepts an RFC 8785 form of the parsed request of at most 1,048,576 octets.
  • The request contains 1..1000 mutations.
  • The RFC 8785 form of each normalized mutation in ADR 002 is at most 65,536 octets.
  • Each mutation contains at most 256 authored columns.

The adapter and extension reject an exceeded limit with 400 invalid_request before ledger allocation, fingerprinting, policy execution, or source DML. Limits are never clamped.

The push response has no octet limit. Each outcome can echo a full authoritative row, so a response can be larger than its request.

The wire contains only mutations with a legal operation-specific precondition. Native clients keep insert successors and other pre-wire dependencies in local durable state until they can normalize them or obtain the predecessor’s server version. depends_on_mutation_id, local supersession, and local cancellation states are not push wire members.

The push envelope contains exactly client_id, client_generation, batch_id, schema, a nonempty mutations array, and an optional atomic member. The Atomic batches section defines atomic. First execution requires the exact current generation and schema. Exact replay uses the sealed values stored with the completed batch.

{
"client_id": "ios-device-123",
"client_generation": 4,
"batch_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674001",
"schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"mutations": [
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674011",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-100"
},
"authored_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"op": "insert",
"client_version": "2026-07-18T13:59:01.000000Z",
"columns": {
"fld_documents_title": "Lower Body"
}
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674012",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-101"
},
"authored_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"op": "update",
"base_version": "opaque-row-version-101",
"client_version": "2026-07-18T13:59:02.000000Z",
"columns": {
"fld_documents_title": "Upper Body"
}
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674013",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-102"
},
"authored_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"op": "delete",
"base_version": "opaque-row-version-102",
"client_version": "2026-07-18T13:59:03.000000Z"
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674014",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-103"
},
"authored_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"op": "update",
"base_version": "opaque-stale-version-103",
"client_version": "2026-07-18T13:59:04.000000Z",
"columns": {
"fld_documents_title": "Tempo Run"
}
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674015",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-104"
},
"authored_schema": {
"version": 5,
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
},
"op": "update",
"base_version": "opaque-row-version-104",
"client_version": "2026-07-18T13:59:05.000000Z",
"columns": {
"fld_documents_legacy_summary": "Preserve this authored value"
}
}
]
}

The request-level schema is the exact current manifest. Each authored_schema is the immutable manifest under which that mutation was created. A client MUST retain the authored reference and every authored field and value. It MUST NOT make queued intent appear compatible by changing the authored schema, dropping a field, or reusing an ID for changed content.

client_version is a required client-authored RFC 3339 UTC diagnostic timestamp. It identifies local intent only. It is not a precondition, stream position, server version, row timestamp source, conflict input, or winner-selection value. Equal, past, and future client timestamps have no conflict authority.

Every mutation requires mutation_id, table, one-field typed pk, authored_schema, op, and client_version. The operation-specific members are exact:

Operation base_version columns
insert MUST be absent REQUIRED object keyed by fields writable under authored_schema. It MAY be empty.
update REQUIRED nonempty opaque token REQUIRED nonempty patch keyed by fields writable under authored_schema
delete REQUIRED nonempty opaque token MUST be absent

Empty update columns, unknown operations, duplicate mutation IDs, duplicate field members, and presence-matrix violations make the whole request 400 invalid_request.

An insert with empty columns authors no field. The supplied primary key identifies the row, and source defaults, identity generation, and BEFORE INSERT triggers supply the other values. A required value that the source does not supply produces terminal validation_failed.

A server that predates the empty-insert rule, such as synchro_pg 0.3.2 or earlier, rejects the whole request that contains such an insert with 400 invalid_request. It does no durable work for that request. The client keeps the sealed batch unchanged and reports the non-retryable protocol error. After the server upgrade, the same sealed batch is accepted.

The server inspects every submitted field after structural validation. A known current primary-key, generated, version, creation-time, update-time, deletion-time, or other non-writable field produces terminal validation_failed. An invalid value or failed current domain constraint also produces terminal validation_failed. The server checks every deferrable constraint at the end of each push unit. A push unit is one mutation of a non-atomic request or the complete group of an atomic request. A failed check at the unit end rolls back that unit and also produces terminal validation_failed. A retained authored table or field that cannot be represented by the current manifest produces terminal schema_incompatible. The server MUST NOT remove any submitted field and report the mutation as applied.

Every HTTP 200 push response requires batch_id, server_time, accepted, and rejected. The batch ID equals the request batch ID. Both arrays are present even when empty. Every request mutation appears exactly once in one array, and the relative request order is preserved within each array. Every outcome echoes mutation_id, logical table, typed pk, and exact outcome_schema.

{
"batch_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674001",
"server_time": "2026-07-18T14:00:01.000000Z",
"accepted": [
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674011",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-100"
},
"outcome_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"status": "applied",
"server_row": {
"fld_documents_deleted_at": null,
"fld_documents_id": "doc-100",
"fld_documents_title": "Lower Body",
"fld_documents_updated_at": "2026-07-18T14:00:01.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "63c3b7ae6054827a3e0d2ca334f8147ae3da135a4b91338daf5d99e704cd3976"
},
"server_version": "opaque-new-version-100"
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674012",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-101"
},
"outcome_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"status": "applied",
"server_row": {
"fld_documents_deleted_at": null,
"fld_documents_id": "doc-101",
"fld_documents_title": "Upper Body",
"fld_documents_updated_at": "2026-07-18T14:00:01.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "5c67d69839a65937e02ae3d898cd34a5a5cb3793706e248ddb604942edb676b4"
},
"server_version": "opaque-new-version-101"
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674013",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-102"
},
"outcome_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"status": "applied",
"server_row": {
"fld_documents_deleted_at": "2026-07-18T14:00:01.000000Z",
"fld_documents_id": "doc-102",
"fld_documents_title": "Archived Plan",
"fld_documents_updated_at": "2026-07-18T14:00:01.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "a7653f643b34a717990175b8e3017488640f833dc4c281079fa34d82e0f11ac5"
},
"server_version": "opaque-new-version-102"
}
],
"rejected": [
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674014",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-103"
},
"outcome_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"status": "conflict",
"code": "version_conflict",
"message": "the base version does not match the current row",
"server_row": {
"fld_documents_deleted_at": null,
"fld_documents_id": "doc-103",
"fld_documents_title": "Current Plan",
"fld_documents_updated_at": "2026-07-18T13:58:00.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "184ee9357371ae6a66f89c5e51152350d0a2a0a1b4c20e0a7756f89cb2331dd6"
},
"server_version": "opaque-current-version-103"
},
{
"mutation_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674015",
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-104"
},
"outcome_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"status": "rejected_terminal",
"code": "schema_incompatible",
"message": "authored mutation cannot be represented by the current schema",
"retryable": false,
"authored_schema": {
"version": 5,
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
},
"current_schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"incompatible_field_ids": [
"fld_documents_legacy_summary"
]
}
]
}

HTTP 200 push success has only these per-mutation statuses:

Status Array Codes Required authoritative data
applied accepted No code New server_version is required. The outcome reports the row state after its push unit, for every operation. A row that exists after the unit, which includes a source tombstone, requires full server_row and matching row_checksum. A row that does not exist after the unit, such as after a fence-only delete, omits both.
conflict rejected version_conflict, row_already_exists, row_deleted, row_not_found message plus current authoritative state. A live row or source tombstone includes row, matching checksum, and version. A fence-only delete includes version without row or checksum. True absence includes neither.
rejected_terminal rejected schema_incompatible, table_not_synced, policy_rejected, validation_failed, atomic_batch_rejected message; no server_row or server_version. schema_incompatible also requires retryable: false, authored and current schema references, and sorted incompatible_field_ids.

Every row in this table also requires outcome_schema. A first execution uses the current request schema. Exact batch replay and a completed mutation reused in a newer batch preserve the original historical binding, so outcomes in one response need not share a schema. The client verifies any row and checksum with that immutable manifest. An unavailable manifest, mismatched digest binding, or unsafe projection into the current local schema fails reconciliation without discarding the outcome or later local intent and requires current-schema rebuild where applicable.

Terminal codes have one meaning:

  • schema_incompatible: preserved authored table or field identity cannot be represented by the current synced schema.
  • table_not_synced: the target has never been a synced table in this contract.
  • policy_rejected: deterministic authenticated write policy denies the mutation. The row security and triggers of the source relation are part of that policy.
  • validation_failed: a known current field is non-writable, a value is invalid, a current domain constraint fails, or a deferrable constraint fails at the end of its push unit.
  • atomic_batch_rejected: another mutation in the same atomic batch did not apply. Only an atomic batch response contains this code.

The complete conflict matrix after structural, schema, policy, and value validation is:

Current row state Version relation insert update delete
absent No current version applied row_not_found conflict row_not_found conflict
live Base equals current row_already_exists conflict applied applied
live Base differs from current row_already_exists conflict version_conflict version_conflict
deleted Any permitted base relation row_deleted conflict row_deleted conflict row_deleted conflict

Insert atomically reserves an absent row identity. Update and delete lock the authoritative row and version identity, compare base_version for byte-for-byte equality, and perform the state transition and new version allocation as one compare-and-swap. An unlocked check followed by an unconditional write is invalid. Concurrent operations cannot both advance the same prior version.

When multiple terminal semantic rules apply, classification order is schema_incompatible, table_not_synced, policy_rejected, then validation_failed. Conflict evaluation occurs only after those checks pass.

A write policy denial by the source relation has rejected_terminal with policy_rejected and no server_row, row_checksum, or server_version. The server detects these denials during row-state evaluation and the source write, after the checks above pass:

  • Row security hides a live target row from the caller. That row is not absent. The rule applies to every operation and base relation. The server identifies a hidden row from its authoritative row version state. A row that this state marks as deleted keeps the row_deleted conflict. A row that another transaction commits during the evaluation is not hidden when the caller can see it.
  • An update or delete changes zero rows, although the server holds the lock on the target row. A row security clause or a BEFORE trigger that returns NULL causes this result.
  • An insert changes zero rows, and the caller can see no conflicting row.
  • A new or changed row fails a row security WITH CHECK expression.

A source write that changes zero rows has no durable effect. The server rolls back each write that a trigger makes during that source write.

These outcomes let a writer learn whether a row that row security hides exists at a given key. PostgreSQL checks unique and primary key constraints without row security, so an insert shows this fact on every server. The outcomes show no row content, checksum, or version. A synced table should use random UUID primary keys, so that a caller cannot guess the keys of rows that it cannot see.

No per-mutation retryable status or code exists. Operational uncertainty is always a whole-request 429 or 503 response with no accepted or rejected arrays.

A request with atomic: true applies all of its mutations or none of them. The value false is invalid, so each meaning has one encoding. An atomic request has the ordinary push limits. The adapter and extension return 400 invalid_request before ledger allocation, policy execution, or source DML when an atomic request has either of these defects:

  • Two mutations have the same table and the same canonical pk.
  • A mutation_id already has a ledger row from a different batch.

The batch fingerprint of an atomic request uses a separate preimage tag, as ADR 002 defines. The server evaluates mutations in request order and stops at the first outcome that is not applied. If every mutation applies, the server checks every deferrable constraint of the group. If every mutation applies and the check passes, the response is an ordinary all-applied response. Each of its outcomes reports the row state after the check. The last mutation owns each write that a deferred trigger makes during the check. Otherwise, the server rolls back every source write, version transition, and write fence of the group, and returns this response:

  • The failing mutation keeps its evaluated status and code, unless the conflict rule below changes it.
  • If the constraint check at the end of the group fails, the last mutation is the failing mutation. It has rejected_terminal with validation_failed.
  • A failing conflict carries the committed row state that the server reads after the rollback. If row security hides that live row from the caller, the failing mutation has rejected_terminal with policy_rejected and no server_row, row_checksum, or server_version.
  • Every other mutation has rejected_terminal, code atomic_batch_rejected, and message atomic batch rejected. It has no server_row, row_checksum, server_version, or retryable, and its outcome_schema is the request schema.
  • accepted is empty, and rejected contains every outcome in request order.

Every validator of an atomic response requires one of two forms. Either every mutation is in accepted with applied, or accepted is empty and exactly one outcome has a code other than atomic_batch_rejected. A non-atomic response never contains atomic_batch_rejected. Exact replay of a completed atomic batch returns its stored response.

Before first transmission, the client persists every UUID, seals the batch, and preserves mutation order and all presence distinctions. A retry sends the exact same batch content.

The server scopes batch and mutation identity to the authenticated user and client, computes canonical SHA-256 request fingerprints, and retains terminal ledger outcomes until irreversible client retirement. The batch fingerprint covers the authenticated scope, client generation, batch UUID, exact request schema, mutation order, and complete mutations. Each mutation fingerprint covers its authenticated scope, UUID, logical table and typed key, operation, authored schema, field presence and values, base version, and client version.

  • Repeating a completed batch_id with the identical fingerprint returns the original HTTP status and exact canonical response body, including original time, order, rows, versions, codes, and messages. It performs no mutation work.
  • Reusing a completed batch or mutation ID for different content returns 409 idempotency_conflict and performs no mutation work.
  • Reusing an unchanged completed mutation in a different new batch contributes its exact stored outcome without re-execution.
  • Reusing a completed atomic_batch_rejected mutation in a different non-atomic batch returns 400 invalid_request and performs no mutation work.
  • Exact completed replay occurs before current client-generation and schema gates, but after authentication, protocol, structural, and permanent-retirement gates.
  • Concurrent requests with one batch identity and fingerprint serialize to one first execution. Later callers receive its exact completed response or a whole-request retryable error, never a second execution.

For first execution, all accepted source writes, opaque version transitions, write fences, mutation outcomes, and the complete replay response commit in one database transaction. Semantic rejections can coexist with accepted mutations. An operational failure rolls back the entire first execution and returns no per-mutation outcomes.

An accepted push response is authoritative for immediate client reconciliation. Push does not synchronously publish pull-visible changes or advance a pull cursor. WAL materialization is the only path that creates pull-visible effects. A later pull delivers the authoritative echo using the same server-owned version transition.

Pull reads one bounded incremental page across assigned scopes. Scope eligibility is resolved before the global limit, and every selected candidate becomes an explicit change or the whole page fails.

{
"client_id": "ios-device-123",
"client_generation": 4,
"schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"scope_set_version": 13,
"scopes": {
"documents_shared": {
"cursor": "opaque-incremental-cursor-c"
},
"documents_user:u_123": {
"cursor": "opaque-incremental-cursor-d"
}
},
"limit": 500
}

The exact request rules are:

  1. client_id and client_generation identify the current authenticated client generation.
  2. schema is the exact current immutable reference.
  3. scope_set_version is the last locally applied assignment version. A value greater than the server version is invalid.
  4. scopes contains each locally active or previously assigned scope being reconciled. Every value has exactly one cursor member containing an opaque string or null.
  5. null means no usable incremental state and requests rebuild for that scope.
  6. A never-assigned or invented scope is invalid. A previously assigned scope that was removed is accepted for authoritative removal dispatch.
  7. limit is an integer in the fixed inclusive range 1..1000. It is never defaulted or clamped.
{
"changes": [
{
"scope": "documents_user:u_123",
"table": "tbl_documents",
"op": "upsert",
"pk": {
"fld_documents_id": "doc-200"
},
"row": {
"fld_documents_deleted_at": null,
"fld_documents_id": "doc-200",
"fld_documents_title": "Tempo Run",
"fld_documents_updated_at": "2026-07-18T14:01:00.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "6f5d85950d90b017b28373562c0f0a2c538abd1d455aaef86289095cc138fcbb"
},
"server_version": "opaque-pull-version-200"
},
{
"scope": "documents_shared",
"table": "tbl_documents",
"op": "delete",
"pk": {
"fld_documents_id": "doc-201"
},
"server_version": "opaque-pull-version-201"
}
],
"scope_set_version": 14,
"scope_cursors": {
"documents_shared": "opaque-next-incremental-cursor-c",
"documents_user:u_123": "opaque-next-incremental-cursor-d"
},
"scope_updates": {
"add": [
{
"id": "documents_team:t_9",
"cursor": null
}
],
"remove": []
},
"rebuild": [
"documents_team:t_9"
],
"has_more": false,
"checksums": {
"documents_shared": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "2233445566778899223344556677889922334455667788992233445566778899"
},
"documents_team:t_9": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "33445566778899aa33445566778899aa33445566778899aa33445566778899aa"
},
"documents_user:u_123": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "1122334455667788112233445566778811223344556677881122334455667788"
}
}
}

Every HTTP 200 pull response requires changes, scope_set_version, scope_cursors, scope_updates, rebuild, and has_more. Arrays and maps are present when empty. checksums follows only the terminal presence rule below.

Pull operations are exactly upsert and delete.

  • An upsert requires a complete authoritative row, matching row_checksum, and opaque server_version.
  • A delete without row is an authoritative local removal for that scope. The client MUST NOT create a local mutation or invent a tombstone.
  • A delete with a source tombstone requires the complete tombstone row, matching row_checksum, and opaque server_version.
  • Every change carries exactly one scope ID, logical table ID, and typed primary key.

The server opens one repeatable-read snapshot, reads the durable materialization progress row first, and fixes its boundary to the visible generation-start or transaction-end point. Synchronized projections, scope effects, and that progress row were committed atomically by the serial WAL materializer, so the snapshot contains exactly materialized state through that boundary. Within the generation, source events are ordered by commit LSN and event ordinal as defined by ADR 001 and ADR 003. Clients do not receive or construct those coordinates, and pull never hydrates from later live source state.

For each incrementally valid assigned scope, the server determines all eligible effects after its presented cursor and through the fixed boundary. Every effect has a persisted delivery position (commit_lsn, event_ordinal, effect_ordinal), so a page can stop between sibling effects from one source event. It deduplicates by (scope_id, logical_table_id, typed_primary_key), retaining the greatest eligible effect for each key. Each earlier effect for that key is covered by that retained candidate for progress accounting. Only then does it apply the global limit through the deterministic ADR 003 merge order.

The merge is starvation-free at the fixed boundary. Repeated pages consume the least remaining eligible candidates, so activity in one scope cannot indefinitely conceal an older eligible candidate in another scope.

Every selected candidate has exactly one result:

  1. A complete upsert from the captured WAL projection.
  2. An explicit delete from an authoritative deletion or scope-removal effect.
  3. No response because the whole page failed.

The server MUST NOT hydrate a historical candidate from a later live row, silently omit a selected candidate, or advance beyond an unrepresented effect.

has_more is true exactly when at least one eligible candidate remains for an incrementally valid scope at the fixed boundary. It is not inferred from the number of raw database rows read.

Scope cursor and acknowledgement semantics

Section titled “Scope cursor and acknowledgement semantics”

scope_cursors is a delta map of newly issued incremental cursors.

  • A scope receives a cursor only through its greatest safe progress position. Every earlier raw effect is either explicitly delivered or covered by the retained later candidate for the same typed key.
  • An unselected retained candidate remains strictly after the issued cursor and keeps has_more true; a cursor never passes it. This prevents a suppressed earlier effect from blocking the same selected candidate forever while preserving eventual delivery of the retained state.
  • A scope blocked by the page limit is not advanced beyond its last represented candidate.
  • On a terminal page, every incrementally valid active scope is accounted through the response boundary and receives a cursor at that boundary.
  • A scope listed in rebuild receives no new incremental cursor.
  • Issuing a cursor does not acknowledge it on the server.
  • A durable server checkpoint advances only when a later successful pull presents that cursor, or a newer valid cursor, for the same scope.
  • An older valid cursor can replay data but never moves a checkpoint backward.
  • A whole-page failure commits neither acknowledgement of presented cursors nor new progress.

This delayed acknowledgement makes response loss safe. Selection, response construction, cursor issuance, and rebuild completion do not prove local apply.

scope_set_version and scope_updates form one authoritative assignment transition.

  • A new assignment appears in scope_updates.add with cursor: null and also appears in rebuild.
  • A removed assignment appears in scope_updates.remove and receives no change or cursor in that response.
  • A row entering an existing scope is a normal scoped upsert.
  • A row leaving an existing scope is a normal scoped delete.
  • The same row in multiple scopes has independent provenance and independent outcomes.
  • A membership contract change invalidates only affected scope generations and lists those scopes in rebuild.

Pull reconciles the authoritative assignment after every request validation passes and before it builds scope_updates. A changed set advances scope_set_version by exactly one, and the same response reports the change. An unchanged set and a failed pull write no assignment state. Rebuild does not reconcile assignment.

The client applies assignment updates, changes, cursor deltas, and terminal integrity results as one local metadata transition.

Checksum presence and key sets are exact:

  1. If has_more is true, checksums MUST be absent.
  2. If has_more is false, checksums is REQUIRED, including on an empty no-change page.
  3. The terminal map key set equals the complete authoritative active scope set after applying that response’s scope_updates.
  4. The map includes active scopes listed in rebuild and excludes removed scopes.
  5. Every value is the exact structured SHA-256 scope checksum at the schema and stream boundary represented by that response.
  6. Missing, extra, malformed, or boundary-mismatched entries invalidate the whole page and advance no local cursor.

A checksum for a scope in rebuild does not make that scope healthy. Rebuild takes precedence. For every other active scope, the client verifies row checksums before apply and recomputes the terminal local scope checksum before installing the new cursor. A mismatch invalidates only that scope’s resumable state and requires rebuild.

An opaque incremental cursor that the server authenticates but recognizes as no longer current is stale. Pull returns HTTP 200, lists that scope in rebuild, omits a new cursor for it, and does not acknowledge it. Other valid scopes in the request can be processed normally.

Ordinary retention-floor movement preserves a cursor at or above the new floor and makes a cursor below the floor stale. A server-recognized obsolete stream, client, schema, membership, or retention lineage also makes the cursor stale rather than forged. An otherwise valid historical-schema cursor avoids rebuild only when connect issues a same-position current-schema replacement through the compatible unaffected-scope bridge; pull never accepts the historical token.

A malformed, forged, misbound, wrong-kind, or unsupported cursor is 400 invalid_request. The entire request fails, no scope is processed, and the response does not identify the failed cursor check.

If an accepted push fence for this client is not yet satisfied by exact WAL materialization or activated verified reset-baseline coverage, pull returns retryable 503 capture_pending. It returns no changes, assignment update, cursor, checksum, or checkpoint advancement. Rebuild uses the same gate and creates no session or page while such a fence is pending. Staged or failed-reset coverage does not clear the gate. A page or terminal checksum can never claim state older than the client’s accepted push.

A transient read failure returns retryable 503 temporary_unavailable. A missing captured projection, digest mismatch, authorization inconsistency, or impossible durable state returns non-retryable 500 sync_integrity_failure and makes readiness unready. Both paths return no partial page or progress.

Rebuild materializes exactly one currently assigned scope from one durable immutable staged snapshot. It does not query live scope state independently for each page.

{
"client_id": "ios-device-123",
"client_generation": 4,
"schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"scope": "documents_shared",
"rebuild_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674101",
"cursor": null,
"limit": 1000
}

scope MUST be currently assigned. rebuild_id is generated and persisted by the client for one rebuild attempt. A new attempt uses a new UUID. cursor is null only on the first request. limit is an integer in the fixed inclusive range 1..1000 and is fixed for the session.

The first request creates or reuses a session for this exact rebuild identity, current accepted-write epoch, and context. In one repeatable-read transaction, the server reads the durable materialization progress row first, fixes the visible generation-start or transaction-end boundary, stages the complete scope from synchronized projections and membership at exactly that boundary, and stores row digests and the final scope checksum in canonical row-identity order. It never stages from live application rows. The staged data and page order are immutable. The session expires exactly 24 hours after creation, and page issuance or replay does not extend it. If complete staging fails, the request returns 503 temporary_unavailable and creates no usable partial session.

{
"scope": "documents_shared",
"records": [
{
"table": "tbl_documents",
"pk": {
"fld_documents_id": "doc-300"
},
"row": {
"fld_documents_deleted_at": null,
"fld_documents_id": "doc-300",
"fld_documents_title": "Shared Plan",
"fld_documents_updated_at": "2026-07-18T14:02:00.000000Z"
},
"row_checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "81d1d576ff6bf0614a821e1c423f672f35a05aa1d4b391e8adaa447473949d2a"
},
"server_version": "opaque-rebuild-version-300"
}
],
"cursor": "opaque-rebuild-continuation-a",
"has_more": true
}

An intermediate page has has_more: true, a non-null opaque rebuild continuation in cursor, and no final_scope_cursor or checksum.

{
"client_id": "ios-device-123",
"client_generation": 4,
"schema": {
"version": 8,
"hash": "21d56a4f2191820e954f008ea32ba8da8568a952cd01a45fd1a3fe16bbe8aaea"
},
"scope": "documents_shared",
"rebuild_id": "018f2b5e-7c42-7a1d-9d31-8a95bd674101",
"cursor": "opaque-rebuild-continuation-a",
"limit": 1000
}

A continuation repeats the same client, generation, schema, scope, rebuild UUID, and limit. Only cursor changes to the exact opaque continuation returned by the preceding page. A raw row key, table name, ordinal, or incremental cursor is invalid.

{
"scope": "documents_shared",
"records": [],
"cursor": null,
"has_more": false,
"final_scope_cursor": "opaque-incremental-cursor-at-rebuild-boundary",
"checksum": {
"algorithm": "sha256",
"version": 1,
"encoding": "hex",
"digest": "25ac19f2bf10bb573ba407d20c254cc2a1a65e5eaaa98f62489b1b4717ebd463"
}
}

A final page has has_more: false, cursor: null, a non-null opaque incremental final_scope_cursor, and the structured SHA-256 checksum for exactly the immutable staged scope. The final page MAY also contain the last records.

Every record has a logical table ID, typed primary key, complete row, matching row_checksum, and opaque server_version. The client verifies every row before apply. It installs final_scope_cursor only after all pages are durably applied and the recomputed complete scope checksum matches.

The rebuild endpoint never writes an incremental server checkpoint. A later successful pull acknowledges the final cursor only when the client presents it for this same scope. Rebuild does not change another scope’s checkpoint, assignment, cursor, membership, retention, or rebuild state.

  • Repeating the exact first request with the same rebuild_id and null cursor during the unexpired session returns the exact stored first page and does not create another snapshot.
  • Repeating a valid intermediate continuation returns the exact stored page for that continuation and limit.
  • Repeating the request that produced the final page returns the exact stored final records, checksum, and final cursor.
  • Replaying any page does not requery live state, extend expiry, or update an incremental checkpoint.
  • Writes after the staged boundary are excluded from the snapshot and arrive through later incremental pull.

The outer request generation and schema gates run before continuation validation. A stale presented generation returns client_generation_expired, and a noncurrent request schema returns schema_mismatch. Once the request envelope is current, an authentic rebuild continuation that is expired or invalidated by its token-bound generation, schema, membership, retention, stream, accepted-write epoch, or snapshot state returns 409 rebuild_restart_required before stored-page replay. The response contains no records, final cursor, or checksum. The client discards the continuation and starts a new attempt with a new rebuild_id and cursor: null.

A malformed, forged, misbound, wrong-kind, or unsupported continuation returns 400 invalid_request. A limit different from the session limit also returns 400 invalid_request. The response does not identify the failed continuation check.

Every non-200 response uses this envelope:

{
"error": {
"code": "capture_pending",
"message": "accepted writes are awaiting capture",
"retryable": true
}
}

code, message, and retryable are required. Clients branch on HTTP status, code, and retryable, not on message text. Messages are bounded and contain no row, mutation, identity, credential, cursor, or continuation value. Error responses contain no success envelope, mutation outcomes, changes, records, cursors, or checksums.

HTTP Code or action Retryable Exact use
200 Connect schema action Not an error Exactly none, replace, rebuild_local, or unsupported.
200 Push outcome partition Not an error Complete accepted and rejected terminal partition, including mixed semantic results, or exact replay.
200 Pull or rebuild page Not an error Complete page only. An authentic stale incremental cursor is represented by rebuild, not an error body.
400 invalid_request false Malformed JSON; duplicate or unknown envelope member; invalid envelope or identity type, typed primary key, UUID, operation shape, scope, or limit; or malformed, forged, or misbound cursor.
400 invalid_schema_reference false Connect schema reference is neither a normal immutable reference nor the exact fresh sentinel. The error includes received_schema.
401 auth_required false Bearer authentication is absent or invalid, or client_id is not bound to the authenticated user.
409 idempotency_conflict false A push batch or mutation identity has a different canonical fingerprint.
409 client_retired false The client identity was irreversibly retired.
409 client_generation_expired false First-execution push, pull, or rebuild did not carry the exact current generation. The error includes numeric current_client_generation.
409 rebuild_restart_required false An authentic rebuild session or continuation is expired or invalidated. The error includes scope_id.
422 schema_mismatch false First-execution push, pull, or rebuild carries a well-formed schema reference that is not exact current. The error includes current_schema and received_schema.
426 upgrade_required false Protocol version or required client runtime is incompatible. A protocol mismatch includes required_protocol_version and received_protocol_version; a runtime mismatch includes minimum_client_version and received_client_version.
429 retry_later true Admission control or rate limiting prevents execution.
500 sync_integrity_failure false Durable materialization or staged state violates the integrity contract. Readiness becomes unready.
503 capture_pending true Pull or rebuild cannot include an accepted push until its fence is durably WAL-materialized or represented by activated verified reset-baseline coverage.
503 temporary_unavailable true An operational failure prevents a complete operation. Capture blocking uses bounded reason: "capture_blocked".

HTTP 429 and 503 responses include a valid Retry-After header. A sender MUST emit an IMF-fixdate or nonnegative integer delay-seconds value. The current adapter emits the integer 5. Native receivers MUST continue to tolerate a fractional nonnegative decimal value as a compatibility input after optional whitespace removal. Receivers reject exponents, signs, leading zeros, missing integer digits, and missing fractional digits. A client retries only when retryable is true, and it preserves the exact sealed push request, presented pull cursors, or rebuild page state for that retry. rebuild_restart_required is non-retryable for the same continuation and requires a new rebuild attempt.

Row and version conflicts are HTTP 200 per-mutation conflict outcomes. They are never HTTP 409. schema_incompatible is an HTTP 200 terminal mutation outcome when the request-level schema is exact current. unsupported is a connect schema action, not an error code. No schema action is inferred from schema_mismatch.

When more than one condition is present, the server uses this order:

  1. Authenticate the bearer context and bind client_id. Failure is 401 auth_required.
  2. Apply protocol and required-runtime compatibility gates. Failure is 426 upgrade_required.
  3. Parse and structurally validate the exact endpoint object. Connect uses invalid_schema_reference for its malformed schema reference; other structural failures use invalid_request.
  4. Reject irreversible client retirement with 409 client_retired.
  5. For push only, perform completed batch and mutation replay lookup. Equal fingerprints replay exactly; unequal fingerprints return 409 idempotency_conflict. An atomic request whose mutation has an equal fingerprint in a different batch returns 400 invalid_request. A non-atomic request whose reused mutation has stored code atomic_batch_rejected returns 400 invalid_request.
  6. Apply the current client-generation gate, then the current-schema gate for first execution. Connect renews an expired generation and performs its schema-action dispatch instead.
  7. For pull, validate incremental cursors. Authentic stale state becomes HTTP 200 rebuild dispatch; malformed, forged, or misbound state becomes 400 invalid_request. For rebuild, authentic stale state becomes 409 rebuild_restart_required; malformed, forged, or misbound state becomes 400 invalid_request.
  8. Apply admission control. Failure is 429 retry_later.
  9. Execute the complete extension operation. Operational rollback is 503 temporary_unavailable; pending WAL visibility is 503 capture_pending; an impossible durable state is 500 sync_integrity_failure.

An exact completed push replay therefore remains exact after later schema or client-generation changes. No failed first execution creates a completed batch response. No pull or rebuild error acknowledges a cursor or installs partial state.