Build

Realtime

Every table has an event stream at /v1/apps/{app}/tables/{table}/stream. It is plain server sent events (SSE): one long HTTP response, one event per row change. No SDK or websocket needed.

Events

Each event has a type, an id, and a JSON body:

id: 42
event: insert
data: {"id":"...","type":"insert","table":"messages","record":{...},"old_record":null,"at":"2026-09-22T18:00:00Z"}
  • event is insert, update, or delete. old_record is set for updates and deletes.
  • ?events=insert,update limits which types you get.
  • A comment line is sent every 15 seconds as a heartbeat, so proxies keep the connection open.
  • The stream follows the table's read policy. With an owner table, a user only hears about their own rows.
  • Up to 100 open streams per app.

Listen with curl

Make a table anyone with the publishable key may read, then listen in the background while a row goes in:

berth tables create --app "$APP" messages body:text:notnull --read public
curl -sN --max-time 6 "https://api.atberth.com/v1/apps/$APP/tables/messages/stream?events=insert" \
  -H "Authorization: Bearer $SECRET_KEY" > events.txt &
STREAM_PID=$!
sleep 2
curl -sS -o /dev/null -X POST https://api.atberth.com/v1/apps/$APP/tables/messages/rows \
  -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"hello over SSE"}'
wait $STREAM_PID || [ $? -eq 28 ]
grep -A 1 '^event: insert' events.txt

--max-time only ends the demo; curl exits with 28 when it hits the limit. In real use, leave the connection open.

Listen from a browser

Browser EventSource cannot set headers, so the stream also takes the publishable key and user token as ?apikey= and ?access_token=. EventSource reconnects on its own and sends Last-Event-ID for you.

const base = "https://api.atberth.com/v1/apps/notes_app";
const params = new URLSearchParams({
  apikey: "bpk_...",
  access_token: session.access_token, // leave out for a public table
  events: "insert,update,delete",
});
const stream = new EventSource(`${base}/tables/messages/stream?${params}`);
stream.addEventListener("insert", (e) => {
  const { record } = JSON.parse(e.data);
  addMessage(record);
});
stream.addEventListener("update", (e) => updateMessage(JSON.parse(e.data).record));
stream.addEventListener("delete", (e) => removeMessage(JSON.parse(e.data).old_record.id));
stream.onerror = () => console.log("stream dropped, EventSource will retry");

The same query parameters work from a shell, which is a quick way to check a policy:

curl -sN --max-time 3 "https://api.atberth.com/v1/apps/$APP/tables/messages/stream?apikey=$PUBLISHABLE_KEY" || [ $? -eq 28 ]

When the access token expires, refresh it and open a new EventSource with the new token.

Resume with Last-Event-ID

If a connection drops, send the last id you saw as a Last-Event-ID header and Berth replays what you missed before going live again.

LAST_ID=$(grep '^id:' events.txt | tail -n 1 | cut -d' ' -f2)
curl -sS -o /dev/null -X POST https://api.atberth.com/v1/apps/$APP/tables/messages/rows \
  -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"sent while you were away"}'
curl -sN --max-time 4 "https://api.atberth.com/v1/apps/$APP/tables/messages/stream" \
  -H "Authorization: Bearer $SECRET_KEY" \
  -H "Last-Event-ID: $LAST_ID" > replay.txt || [ $? -eq 28 ]
grep 'sent while you were away' replay.txt

From the CLI

berth watch prints each event as it arrives until you press Ctrl+C:

berth watch --app "$APP" messages --events insert,update,delete

What is not included

Streams carry row changes only. There are no presence or broadcast channels. For chat style fan out, insert a row and let every client stream the table.