Skip to content

POST /sync:pull

Fetch the changes since the cursor

Returns upserts, tombstones, and revocations in a stable order. The cursor is opaque. If it is older than the minimum tombstone period, the server answers sync.cursor_too_old; the client must then resynchronise completely. A cursor this installation did not mint is sync.cursor_invalid.

The same records the stream carries, in the same order and under the same cursor - the stream is an accelerator over this, not a second source of truth. Records are filtered by what the caller may read, per record and at the moment of the read; has_more says the page was full and the client comes straight back. A page may carry fewer records than limit and still have more: the cursor advances past what the caller may not see.

A null cursor is an initial synchronisation: everything the caller may read, as UPSERT records carrying whole objects, paged, and ending in a cursor a delta resumes from.

Request body application/json — SyncPullRequest

NameTypeRequiredDescription
device_idstring (uuid) requiredThe device asking, minted by the client (UUIDv7) and kept in its own store.
platformstring, or null
  • maxLength: 40
optionalWhat the device is - ios, android, desktop-macos, hubctl - for the device list.
display_namestring, or null
  • maxLength: 120
optionalHow the device introduces itself in the list - "Anna's phone".
cursorstring, or null optionalOpaque; NULL means an initial synchronisation.
scopesarray of object optionalWhat the device wants to hold.
container_idstring (uuid) optional
depthstring
  • SELF
  • CHILDREN
  • SUBTREE
  • default: "SUBTREE"
optionalCounted in containers.
limitinteger
  • default: 500
  • maximum: 2000
optional

Responses

Codes this operation names

Example bash
curl -X POST "$HUBTASK_URL/sync:pull" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "device_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "platform": "string",  "display_name": "string",  "cursor": "string",  "scopes": [    {}  ],  "limit": 500}'

POST /sync:snapshot

The initial synchronisation as one stream

What :pull with no cursor answers in pages, as one response: the records of the initial synchronisation in the walk's order, one per line of application/x-ndjson, each the same SyncChange a page would carry, written as they are read so that the first byte arrives before the last row is counted, and the delta cursor as the last line in a record of its own - {"cursor": "…"} - minted from the log position read before the first row, exactly as a page sequence's is. Permission is checked per record, a scope narrows the walk, and the device is registered by turning up.

The response is bounded the way the stream is: a deadline, a byte budget per connection, and the connection counted with the streams' - refused with 503, sync.stream_unavailable and a Retry-After where a pod, a workspace or a credential holds its complement. A device that lost the connection before the last line has no cursor and starts again, which is what a page sequence offers it too; the difference is one request rather than a thousand. A connection that ends before the cursor line never learns the cursor - which is the point: a snapshot a device did not read whole is not one it may resume from.

Request body application/json — SyncSnapshotRequest

NameTypeRequiredDescription
device_idstring (uuid) requiredThe device asking, as on :pull; it registers by turning up.
platformstring, or null
  • maxLength: 40
optional
display_namestring, or null
  • maxLength: 120
optional
scopesarray of object optionalWhat the device wants to hold, as on :pull; no scope means everything the caller may read.
container_idstring (uuid) optional
depthstring
  • SELF
  • CHILDREN
  • SUBTREE
  • default: "SUBTREE"
optional

Responses

  • 200 The records, one per line, and the cursor last
  • 503 An error per RFC 9457 Problem
  • 4XX An error per RFC 9457 Problem

Codes this operation names

Example bash
curl -X POST "$HUBTASK_URL/sync:snapshot" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "device_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "platform": "string",  "display_name": "string",  "scopes": [    {}  ]}'

POST /sync:push

Transmit local mutations

Idempotent through op_id. Merging happens per field; set fields as OR-sets and positions as fractional indices. Partial success is the normal case: rejected mutations do not block the others.

Request body application/json — SyncPushRequest

NameTypeRequiredDescription
device_idstring (uuid) requiredThe device pushing; registered the way a pull registers it.
platformstring, or null
  • maxLength: 40
optional
display_namestring, or null
  • maxLength: 120
optional
mutationsarray of SyncMutation
  • maxItems: 500
required
op_idstring (uuid) requiredThe idempotency key.
kindstring
  • ITEM_CREATE
  • ITEM_PATCH
  • ITEM_DELETE
  • SET_ADD
  • SET_REMOVE
  • MOVE
  • COMMENT_ADD
required
item_idstring (uuid) optionalAssigned by the client (UUIDv7), including on CREATE
base_versioninteger, or null optional
fieldsobject optionalPer-field changes, each field with its own HLC.
setstring, or null
  • labels
  • members
  • watchers
  • attachments
  • null
optionalWhich set of the entry the element belongs to.
elementstring (uuid), or null optional
hlcstring, or null optional
payloadobject, or null optional

Responses

Example bash
curl -X POST "$HUBTASK_URL/sync:push" \  -H "Authorization: Bearer $HUBTASK_TOKEN" \  -H "Content-Type: application/json" \  -d '{  "device_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",  "platform": "string",  "display_name": "string",  "mutations": [    {      "op_id": "018f6e2a-2b7c-7c1e-9b1a-3d5e6f7a8b9c",      "kind": "ITEM_CREATE"    }  ]}'

GET /sync/devices

List your own devices

The caller's own devices, most recently seen first - and never anybody else's, whatever the role: a device is the person's, like a session. A forgotten device stays in the list as blocked until the sweep removes it, so that a person can see what they ended.

Responses

  • 200 The devices
Example bash
curl -X GET "$HUBTASK_URL/sync/devices" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

DELETE /sync/devices/{deviceId}

Forget one of your devices

Ends what the device holds: the session it last synchronised under is revoked, and every push or pull from the identifier is refused with sync.device_revoked until the client mints a new one. The row stays, marked blocked, until the retention sweep removes it. Somebody else's device is not found rather than forbidden; forgetting twice is not an error.

Path parameters

NameTypeRequiredDescription
deviceIdstring (uuid) required

Responses

  • 204 Forgotten
  • 4XX An error per RFC 9457 Problem

Codes this operation names

Example bash
curl -X DELETE "$HUBTASK_URL/sync/devices/{deviceId}" \  -H "Authorization: Bearer $HUBTASK_TOKEN"

GET /stream

The change stream, as server-sent events

The same change records :pull returns, pushed as they happen. An accelerator, not a second source of truth: a client that loses this connection catches up by cursor rather than by anything being replayed from memory, and a client that never opens it is not missing anything a pull would not give it.

text/event-stream. Each event carries id (the cursor), event (the entity), and data (one change record). The id is what a browser's EventSource sends back as Last-Event-ID on its own reconnect, and what a client that manages its own connection should send explicitly — either resumes exactly where the stream stopped, with no gap and no duplicate.

A cursor older than the tombstone period is refused with sync.cursor_too_old, exactly as :pull refuses one: a delta across a gap would be silently wrong, so the only safe answer is a full resynchronisation. A cursor this installation did not mint is sync.cursor_invalid.

The stream carries everything the caller may read in their workspace, and there is no subscription filter: what a client wants to see is a question about its own screen, and a filter here would be a second answer to a question the authorisation already answers. Records are filtered by what the caller may read, per record and at the moment the record is sent, rather than by anything the subscription was trusted to state. Permission lost while the connection is open therefore stops the records for that container without the client having to do anything, and a client is never handed a record for a container it may not see.

The stream is capped per tenant and per credential, because a long-lived connection is a resource a stateless, horizontally scaled role has to bound. Over the cap, or while the process is shedding load, the answer is 503 with Retry-After — the connection is refused, never a record dropped.

A comment line is sent as a heartbeat so that proxies do not close an idle stream, and SIGTERM closes every stream inside the grace period rather than cutting them: a client reconnects to another pod and resumes from its cursor.

Headers

NameTypeRequiredDescription
Last-Event-IDstring optionalThe cursor to resume from, as the stream last sent it.

Responses

  • 200 The stream, until the client or the server closes it
  • 503 Over the stream cap, or the process is shedding load Problem
  • 4XX An error per RFC 9457 Problem

Codes this operation names

Example bash
curl -X GET "$HUBTASK_URL/stream" \  -H "Authorization: Bearer $HUBTASK_TOKEN"