Realtime
Subscribe to a table's changes over Server-Sent Events, each event re-read as the subscriber with policy and redaction applied.
Subscribe to a table and receive its changes as they happen, over Server-Sent Events (SSE). There is no realtime API to call from your code: writes are the producers. Every create, update and delete through the REST API, the handler SDK or an agent is recorded and fanned out to the table's subscribers.
invoices, vendors, payments). Your app has only the tables you create, so put your own table and column names in their place. About the example appGET /{appUid}/autocrud/{table}/subscribe
Authorization: Bearer <token>
Accept: text/event-streamOpening the stream is gated by the same {table}:read_list policy as listing the table: if you
cannot list it, you cannot subscribe to it. A switched-off List operation (TABLE_LOCKED) closes
this door too.
Each event's row is then read the way a single-row GET is, so subscribing needs both
{table}:read_list and {table}:read, with the table's List and Read one operations switched on.
Send the Accept header
A request without Accept: text/event-stream is refused with 406. Most SSE client libraries send
it for you; a plain fetch or curl does not.
Connecting
SSE here is an ordinary authenticated request, so read it with a client that can set an
Authorization header. In a browser that means fetch, not EventSource, which cannot send one.
# -N disables buffering, so events print as they arrive
curl -N "$AUTHORIC_URL/$APP_UID/autocrud/invoices/subscribe" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"A library such as @microsoft/fetch-event-source does the framing, the header and the
reconnect for you in a browser.
What arrives
event: connected
data: {"table":"invoices","live":true}
id: 1013
data: {"op":"insert","table":"invoices","row":{"uid":"0199b8e4-2a6f-7c31-8d5e-41f0a9c37b12","invoice_number":"MS-88461","vendor_uid":"0199b8e1-9c04-7a8b-b2d3-6e5f1c8a4d07","status":"received","amount":"8760.00"}}
id: 1014
data: {"op":"update","table":"invoices","row":{"uid":"0199b8e4-2a6f-7c31-8d5e-41f0a9c37b12","invoice_number":"MS-88461","vendor_uid":"0199b8e1-9c04-7a8b-b2d3-6e5f1c8a4d07","status":"matched","amount":"8760.00"}}
id: 1015
data: {"op":"delete","table":"invoices","rowUid":"0199b8e4-2a6f-7c31-8d5e-41f0a9c37b12"}| Frame | Carries |
|---|---|
event: connected | Sent once the stream is open. |
insert, update | The whole row as this subscriber may read it: named by uid, references as _uid, redacted columns as "[redacted]". The same shape as a GET. |
delete | Only rowUid. The row is gone (or soft-deleted), so there is nothing to re-read. |
event: resync | Refetch the list, then carry on. reason says why: backfill_truncated (you were too far behind to catch up from the log), backfill_expired (you resumed from before the oldest change the log still keeps) or bulk_change (a bulk update or delete changed rows without naming each one). |
event: reauth | Your access token expired and the stream is closing. Reconnect with a fresh token. |
: ping | A comment every 25 seconds, so idle connections are not cut by proxies. Ignore it. |
Each id: is a position in the table's change log, not a record id. Keep the last one you
processed; it is how you resume.
Treat insert and update the same way, as an upsert keyed by row.uid. An update to a row your
client has never seen arrives as an update, and you may see the same event twice around a
reconnect.
Policy is applied to every event
This is the part worth understanding.
An event is not broadcast because you were allowed to subscribe a minute ago. For each change,
the row is re-read as the subscribing principal, through the same governed read as a GET:
your organization, your row-level policy and the table's column redaction, applied at that
moment. A row you may not read is skipped silently; you never learn it changed.
A delete is judged the same way. The row can no longer be read, so it is judged as it stood when
it was deleted: you receive its rowUid only if your organization, your row-level policy and your
{table}:read grant would have let you read that row. A bulk update or delete is judged on the
organization it ran in, and arrives as a resync.
Why this matters
The common design authorizes the subscription once and streams from then on. Revoke someone's access mid-stream and they keep receiving data. Here, a change to policies, roles or role bindings tells every open stream on the app to drop what it knew, so the next event is judged on the new rules.
A consequence: two subscribers to the same table can legitimately receive different events, and the same event with different columns redacted. The row itself is re-read for every event.
Reconnecting without gaps
Changes are written to a durable log in the same transaction as the write, so a dropped connection
loses nothing. When you reconnect, send the last id you processed as the Last-Event-ID header
(or as ?lastEventId= in the URL) and the stream replays what you missed before going live.
The replay is bounded at 1,000 events. If you were further behind than that, you get a resync
frame naming the last id replayed: refetch the list with a normal read and continue from the
stream. The log keeps a week of changes; resuming from an id older than that sends resync with
reason: "backfill_expired" first, then replays what remains.
Streams end with their token
A stream is authenticated once, when it opens, and it lives no longer than the access token that
opened it. At the token's expiry you get event: reauth with {"reason":"token_expired"} and the
stream closes. Refresh the token, then reconnect with Last-Event-ID and you miss nothing. See
Sessions.
What produces events
- Produces events: the REST API's create, update and delete; the handler SDK's CRUD methods, bulk writes and CSV import; agents, which write through the SDK; and the console's table editor, CSV import and seeding, each change filed under the organization its row belongs to.
- Does not: raw SQL from a handler (
sdk.sql,sdk.query), which writes inside the database and is not streamed. Write through the SDK's CRUD methods when a change must reach subscribers.
You receive the changes made within your own organization.
Events say "a row changed". They are not a message bus. To push a notification, write a row to a
table (say notifications) and subscribe to that.