API reference · Hubtask API 0.1.0
sync
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
Responses
200The changes SyncPullResponse409An error per RFC 9457 Problem
Codes this operation names
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
Responses
Codes this operation names
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
Responses
200The result per mutation SyncPushResponse400An error per RFC 9457 Problem
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
200The devices
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
Responses
204Forgotten4XXAn error per RFC 9457 Problem
Codes this operation names
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
Responses
Codes this operation names
curl -X GET "$HUBTASK_URL/stream" \ -H "Authorization: Bearer $HUBTASK_TOKEN"