Quickstart: durable stream

Last updated 29 October 2026

View .md

From sign-in to a stream you append to, read, tail live and close, with curl and jq.

Run it in a terminal on your laptop or on any server with internet access. A stream is reached over HTTPS at a public address, with nothing to install beyond the CLI, curl and jq, and nothing to allowlist; the Streams overview says where else it runs.

Sign in

Sign in at tonbo.dev with GitHub or Google. A new account asks you to choose a username: it names your personal account, which holds your databases, streams and keys, and appears in the console’s addresses. You can change it once every 90 days.

Create a stream

Create a stream named events for JSON messages. Its content type is fixed for its life, and every append sends exactly it.

  1. Open Streams in the console.
  2. Choose Create stream, name it events and set Content type to application/json.

The stream is creating for a moment, then active, at a URL on its region’s Streams origin:

https://streams.tonbo.dev/<account-id>/s.events

Create a key

Every request to the stream carries an API key. With the CLI (npm i -g @tonbo/cli, then tonbo login), create a key that acts as you:

tonbo api-key create events-quickstart --expires-in 15_days

tonbo login signs you in through a browser on the same machine. On a machine without one, such as a remote server, set TONBO_API_KEY to a key that acts as you and is not narrowed, created in the console under API keys, and the CLI uses it instead.

It prints the key once, as TONBO_API_KEY=tbo_.... This key acts as you and is not narrowed, so the CLI may keep using it: put it in your shell, and keep the stream’s URL in STREAM:

export TONBO_API_KEY=tbo_...
STREAM=$(tonbo stream show events --json | jq -r .url)

Each request below sends the key as Authorization: Bearer $TONBO_API_KEY. Keep the key on your machine or your server; to give a program a key that reaches only its own streams, or a web page a token that expires in minutes, see Access.

Append

Append two messages, each with a producer id, an epoch and the next sequence number, which name the writer so a retry applies once (see A producer):

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 0" \
  -d '{"n":0}'

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 1" \
  -d '{"n":1}'

Each answers 200 with Stream-Offset and Stream-Next-Offset, where its message starts and ends. Send the second one again, as a client that never saw its answer would: it answers 204 with the same two offsets and appends nothing, so a retry never writes a message twice.

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 1" \
  -d '{"n":1}'

Read

Read the stream from the start:

curl -sS -i "$STREAM?offset=-1" \
  -H "Authorization: Bearer $TONBO_API_KEY"

It answers 200 with the two messages, one JSON value per line, and Stream-Up-To-Date: true. Its Stream-Next-Offset is the end of the stream: keep it for the next step.

NEXT=<the Stream-Next-Offset value>

Tail it

Open a tail at the end, and it waits for what is appended next. Pick one mode and run it:

curl -sS -N "$STREAM?offset=$NEXT&live=sse" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Accept: text/event-stream"

The connection stays open and prints an event: data for each new message, then an event: control with the next offset.

While it waits, append the third message from a second terminal with the same STREAM and TONBO_API_KEY:

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Producer-Id: quickstart" \
  -H "Producer-Epoch: 0" \
  -H "Producer-Seq: 2" \
  -d '{"n":2}'

The tail shows it at once. Stop an SSE tail with Ctrl-C.

Close it

Closing is permanent: the stream keeps everything and stays readable, and refuses every append after it.

curl -sS -i -X POST "$STREAM" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Stream-Closed: true"

It answers 204 with Stream-Closed: true. From now on every read at the end says the stream is over: a read answers 200 with no messages and Stream-Closed: true, a long-poll answers 204 with Stream-Closed: true at once instead of waiting, and an SSE read gets a control event with streamClosed: true before the server ends it. An append answers 409.

Delete it

Deleting removes the stream and everything in it, and cannot be undone. The CLI asks you to type the name:

tonbo stream delete events

Through the API, the same is DELETE https://api.tonbo.dev/v1/streams/events. Once it is deleted, the stream’s URL answers 404 and the name is free again.

Add it to a project

In a project of your own, with the program’s API key in TONBO_API_KEY where the project keeps its secrets, never in its code:

  1. Create the stream with tonbo stream create events --content-type application/json, signed in with tonbo login, or in the console. tonbo stream show events prints State: active once it is ready, and tonbo stream show events --json | jq -r .url prints its URL: put it beside the key.
  2. The program appends with a POST to the URL, with Authorization: Bearer and the key, the stream’s content type, and Producer-Id, Producer-Epoch and Producer-Seq, so a retry applies once (see Append).
  3. It reads with a GET to the URL, from ?offset=-1 for the start (see Read).

Next steps