Quickstart
This walks through the whole product in a few curl commands: run the server, write
records, read them back from a cursor, follow a live SSE stream, fan out with a router, and
delete. Everything is plain HTTP and JSON — no client library required.
The examples assume auth is disabled (dev mode), which is the default on a loopback bind,
and that TOPICS points at the server. See Install & Run
and Configuration for production settings.
Run the server
Binary
# Build and run (Rust toolchain required). Defaults: bind 127.0.0.1:4000,
# auth disabled (dev mode); WAL + segments + snapshots under ./topics-data.
cargo build --release
./target/release/topicsOnce it’s listening, point your shell at it so the rest of the commands stay short:
export TOPICS=http://localhost:4000Create a topic (optional)
A topic is created lazily on first write, so this step is optional — but it lets you set config up front. Here we make a durable, unbounded log of an online store’s orders.
curl -X PUT $TOPICS/v0/topics/orders \
-H 'content-type: application/json' \
-d '{ "durable": true, "cap_records": 0, "ttl_ms": 0 }'{ "topic": "orders", "created": true,
"config": { "ttl_ms": 0, "cap_records": 0, "cap_bytes": 0, "discard": "old",
"durable": true, "durability": "fsync", "auto_create": true,
"idempotency_window_ms": 120000, "dedupe_node": true },
"performance": { "server_total_ms": 0.22 } }Write records
The server assigns each record a $seq (a monotonic u64) and a $ts. A single POST
can carry up to 10,000 records, committed atomically. An optional tag gives each record a
match key you can delete by later.
curl -X POST $TOPICS/v0/topics/orders \
-H 'content-type: application/json' \
-d '{ "records": [
{ "data": { "sku": "AEROPRESS-GO", "qty": 1, "total": 3499 }, "tag": "order-7731" },
{ "data": { "sku": "FELLOW-KETTLE", "qty": 1, "total": 16500 }, "tag": "order-7732" }
] }'{ "topic": "orders", "first_seq": 1, "last_seq": 2, "seqs": [1, 2],
"head_seq": 2, "count": 2, "created": false, "deduped": false,
"performance": { "server_total_ms": 0.62, "fsync_ms": 0.39 } }Read from a cursor
The core consume operation. Pass a from_seq cursor; get the records after it, a
continuation cursor, and any tombstone. The monotonic seq is the cursor — there is no
opaque token. Advancing your stored from_seq is the ack.
curl -X POST $TOPICS/v0/topics/orders/diff \
-H 'content-type: application/json' \
-d '{ "from_seq": 0, "limit": 500 }'{ "topic": "orders",
"records": [
{ "$seq": 1, "$ts": 1748470000123, "$tag": "order-7731", "data": { "sku": "AEROPRESS-GO", "qty": 1, "total": 3499 } },
{ "$seq": 2, "$ts": 1748470000140, "$tag": "order-7732", "data": { "sku": "FELLOW-KETTLE", "qty": 1, "total": 16500 } }
],
"next_from_seq": 2, "head_seq": 2, "earliest_seq": 1,
"caught_up": true, "tombstone": null, "lag": 0,
"performance": { "server_total_ms": 0.30 } }A reader that’s caught up gets caught_up: true — and because skipped records (deleted,
expired, node-filtered) still advance the cursor, caught_up, not records.length, is the
reliable “nothing more right now” signal. Tag a write with a node id and a reader passing
that same node won’t receive its own events back; that’s how mirrored nodes stay echo-free
— see node loop-prevention.
Follow a live SSE stream
POST /v0/watch creates a session and returns a wid; a GET on that wid opens the
EventSource-compatible stream.
# 1. Create the watch session (carries the full subscription).
curl -X POST $TOPICS/v0/watch \
-H 'content-type: application/json' \
-d '{ "topics": { "orders": { "from_seq": 0 } } }'
# → { "wid": "wid_BuRguGorNdVFWNQULz-rrw", "stream_url": "/v0/watch/wid_BuRguGorNdVFWNQULz-rrw", ... }
# 2. Open the stream.
curl -N $TOPICS/v0/watch/wid_BuRguGorNdVFWNQULz-rrwretry: 2000
id: eyJvcmRlcnMiOjF9
event: record
data: {"topic":"orders","records":[{"$seq":1,"$ts":1748470000123,"data":{"sku":"AEROPRESS-GO","qty":1,"total":3499}}],"from_seq":0,"to_seq":1,"head_seq":2}
id: eyJvcmRlcnMiOjJ9
event: caught-up
data: {"topic":"orders","head_seq":2}
: hbFan out with a router
A router copies every record appended to source into dest — at-least-once, per-source
FIFO. The origin $node rides through untouched, so symmetric nodes mirror without echo.
Here, every order is mirrored into a fulfillment topic a warehouse service can consume on
its own.
curl -X PUT $TOPICS/v0/routers/orders-to-fulfillment \
-H 'content-type: application/json' \
-d '{ "source": "orders", "dest": "fulfillment" }'Delete records (permanent, point-in-time)
A delete removes records that exist right now, by seq range and/or tag match. It is
permanent, silent (never a tombstone), effective immediately on all reads, and point-in-time
(future records with the same tag are never affected).
# A customer cancels — remove the order by its exact tag.
curl -X POST $TOPICS/v0/topics/orders/delete \
-H 'content-type: application/json' \
-d '{ "match": ["tag", "Eq", "order-7731"] }'{ "topic": "orders", "deleted": 1, "earliest_seq": 2, "head_seq": 2, "count": 1,
"performance": { "server_total_ms": 0.12 } }Next steps
- Understand the five concepts in depth.
- Read the Core Guarantees — durability, tombstones, deletion, ordering.
- Browse the full API Reference, or jump to a recipe: Job Queue, Pub/Sub, Durable Streams.