Skip to Content
Getting StartedQuickstart

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

# 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/topics

Once it’s listening, point your shell at it so the rest of the commands stay short:

export TOPICS=http://localhost:4000

Create 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-rrw
retry: 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} : hb

Fan 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

Last updated on