Four build phases, each with crisp acceptance criteria, plus the benchmark plan. The guiding rule: the API and its semantics are fixed in phase 1 and never change; later phases add persistence, tests, and scalability underneath an unchanging contract. See API.md, DESIGN.md, ARCHITECTURE.md.
Produce the complete, internally consistent specification the implementation must satisfy.
Deliverables
README.md— pitch, mental model, quickstart, use-case recipes, feature list, phases, doc links.docs/API.md— the complete/v0HTTP reference (the contract).docs/DESIGN.md— data model & semantics (seq, dual watermark, tombstones, permanent deletion, node loop-prevention, routers, priority).docs/ARCHITECTURE.md— storage, WAL, group commit, segments, recovery, scheduler, concurrency, crates, latency budget.docs/ROADMAP.md— this file.
Acceptance criteria
- Every endpoint in the §9 index of API.md has a documented method, path, request, response, and error set.
- Field names, config keys, defaults, and status codes are identical across all four docs
(one vocabulary:
cap_records/cap_bytes/discard/durable/priority+auto_priority,from_seq/next_from_seq/head_seq/earliest_seq/evict_floor, deletion viabefore_seq/match(Eq/Glob), deletion described as permanent/async/silent/point-in-time, tombstonereason ∈ {cap, ttl, mixed, recreated, from_seq_too_old},metafor record headers,$-prefixed server fields). - The three use-case recipes (job queue / pub/sub / strong delivery) are expressible purely with documented endpoints.
- The seven safety invariants (DESIGN §9) are stated and traceable to specific mechanisms.
A correct, complete implementation of the entire /v0 API with all data in memory. Not
persistent, not yet scalable — but every endpoint, every semantic, and every error path works.
Scope
- Full HTTP surface (axum/hyper): topics CRUD, write, diff, delete (
before_seq/match), routers CRUD, SSE (POST-create + GET-stream), health/ready/metrics. - In-memory
TopicIndex(base+offset vector) + per-topic tag index, dual floor (earliest_seq/evict_floor) +epochatomics. - Cap (
cap_records/cap_bytes) + TTL eviction advancingevict_floorwith in-band tombstones. discard: "old" | "reject"full-topic policy (422 topic_full).- Permanent deletion via
POST .../delete(before_seqsnapshot, tagmatchexact +tag*prefix, combined): point-in-time, silent (no tombstone), effective immediately on reads,count/bytes/earliest_sequpdated synchronously, lazy front-reclaim of dead slots. Node loop-prevention, cursor-advance reads. - Routers: at-least-once forwarding, per-source FIFO, DAG cycle check (
409),allow_cyclehop cap. - Multiplexed SSE: named events (
record,tombstone,caught-up,topic-deleted,error), compositeid:cursors, heartbeats,retry:, resume viaLast-Event-ID. - Idempotency keys,
performanceblocks, error envelope, auth bearer. - Priority scheduler present in simplified form (banded ready-set + recency); no fsync to gate.
Explicitly out of scope (phase 4): WAL, fsync/durability gating, segments, restart recovery,
metadata snapshots, elastic throttling under real CPU pressure. durable: true is accepted but is
a no-op fast path in phase 2 (documented).
Acceptance criteria
- A conformance test suite drives every endpoint and asserts the documented JSON shapes and status codes.
- Tombstone behavior verified: a consumer whose
from_seq + 1falls belowevict_floorafter cap eviction and after TTL expiry receives the correctreason,gap_from,gap_to; a consumer whose cursor fell into a purely-deleted gap (belowearliest_seqbut at/aboveevict_floor) receives no tombstone (tombstone: null) and the cursor advances silently. - Node loop-prevention verified: a node never receives its own records via
difforwatch, and the cursor advances past them (caught_upreached, not an infinite empty loop). - Deletion verified:
before_seq(snapshot), tagmatchexact, andtag*prefix remove the matching records present at call time from all subsequent reads and SSE;count/bytes/earliest_sequpdate immediately; the delete is point-in-time (a later record with the same tag is NOT deleted); it is permanent (no un-delete) and silent (no tombstone for the deleted seqs); cap/TTL eviction STILL emits a tombstone (deletion never touchesevict_floor). - Router fan-out verified: a write to
srcappears in alldsttopics with$nodepreserved; a cycle-creating router is rejected409; anallow_cyclemirror terminates via the hop cap. - SSE verified: multi-topic stream delivers
record/tombstone/caught-up/heartbeat frames; a reconnect withLast-Event-IDresumes all per-topic cursors atomically. - Server starts, serves, and shuts down cleanly; restart loses all data (expected, documented).
Lock in correctness and record initial performance numbers against the phase-2 server.
Scope
- Unit tests for every module (index, eviction, deletion + tag index, scheduler, SSE framing, cursor math).
- Property/fuzz tests for the seq/dual-watermark/tombstone invariants over randomized
write/evict/expire/delete/read sequences (e.g. "a consumer reading from any
from_seqeither sees a strictly-increasing stream with the deleted/expired/own-node seqs silently skipped, or exactly one tombstone iff its cursor fell belowevict_floor— never silent involuntary loss, and never a tombstone for a purely-deleted gap"). - Integration tests for the use-case recipes end to end.
- A criterion-based benchmark suite (see Benchmark plan below).
docs/BENCHMARKS.mdrecording the initial baseline numbers (in-memory phase-2), with hardware and methodology noted.
Acceptance criteria
- Line/branch coverage target met on the core engine modules (goal: ≥ 90% on index/eviction/deletion/scheduler).
- Invariant property tests pass over randomized write/evict/expire/delete/read sequences.
- The benchmark suite runs reproducibly and
docs/BENCHMARKS.mdcontains baseline figures for every metric listed below. - No test depends on wall-clock sleeps for correctness (use injected clocks for TTL/priority).
Add persistence and scale underneath the unchanged API, staying a single restartable process.
Scope
- WAL: framing (§ARCHITECTURE 2.1), sharded writers (
TOPICS_WAL_SHARDS, defaultmin(num_cpus, 8); each shard an ordered writer with its own file set), adaptive group commit, per-topic durable fsync, preallocation, rotation; shard-count-agnostic recovery. - Compactor: WAL→segment checkpointing; segment
.data/.idx; mmap serving of sealed segments, bufferedpreadof the active one, WAL-direct serving of the newest records. - Segment-granular lazy cap/TTL eviction + persisted
EvictWatermark(advancesevict_floor). - Async deletion (background, ARCHITECTURE §3.5), no compaction / no per-record reclaim: a
delete flips an in-place delete-flag byte in segment files (the WAL stays append-only — a
Deleteframe is appended, never mutated); a whole segment is dropped only when a delete clears it entirely. There is no partial-segment rewrite and no general reclaim of marked records.Deletecontrol frames replay deterministically (idempotent across crashes). - Metadata store: WAL control frames + atomic bincode snapshots; interned
topic_ids. - Restart recovery: snapshot load → segment
.idxbulk load + tag-index rebuild → WAL replay (all shards by topic_id) from last checkpoint (incl.Deleteframes) → XXH3-64 torn-tail truncation → idempotent segment reclaim. - Full priority scheduler: banded DWRR + aging; governor-driven elastic throttling
(coalesce → widen group commit → defer low priority →
429). - Slow-consumer isolation for SSE (bounded channels, lagged-degrade-to-tombstone).
Acceptance criteria
- Durability: for
durable: truetopics, an acked write survives a hard kill (SIGKILL) at any instant and is present after restart; no acked durable write is ever lost. - Crash consistency: a kill during a write leaves the WAL recoverable — recovery truncates the torn tail (XXH3-64 checksum / length), and no partial frame is ever interpreted as data.
- Recovery correctness: after restart,
head_seq/earliest_seq/evict_floor/count, config, routers, and the set of deleted records match the pre-crash state (modulo un-fsynced non-durable tail); previously-deleted records stay gone after replay of theirDeleteframes. - No silent loss across restart: a consumer whose cursor fell below the recovered
evict_floorreceives a tombstone, never silent skip; a cursor in a purely-deleted gap stays silent. - Eviction is segment-granular; deletion is no-compaction / no-reclaim: physical occupancy
may transiently exceed cap by ≤ one segment, and deleted records stay on disk just marked
(only whole cleared segments are dropped — no per-record reclaim);
earliest_seq/count/bytesalways report the live logical floor regardless. - Latency target met: non-durable /
eventualSSE delivery p99 ≤ 5 ms at a defined sustained load (see benchmark plan); durable write-ack p99 within budget with adaptive group commit. - Elastic throttling: under induced CPU pressure, high-priority topics keep their latency while
low-priority topics degrade visibly (
429/ SSEerrorframes), with zero data loss attributable to throttling. - No regressions: the full phase-3 test suite still passes;
docs/BENCHMARKS.mdis updated with phase-4 numbers alongside the phase-2 baseline.
Measured with criterion (micro) + a load harness (macro). Each metric is recorded for the in-memory
baseline (phase 3) and re-recorded for the persistent build (phase 4), so the cost of durability is
explicit. Results land in docs/BENCHMARKS.md with hardware (CPU model, NVMe model), OS, build
flags (--release), and methodology.
| Metric | What | How |
|---|---|---|
| Write throughput | records/s and MB/s appended | sustained POST batches at varied batch sizes (1, 10, 100, 1000) and payload sizes (64 B, 1 KiB, 64 KiB); durable vs non-durable. |
| Append latency p50/p99/p999 | end-to-end ack latency | single-record and batched writes, durable (fsync-gated, group-committed) vs non-durable; report the group-commit batch-size distribution. |
| getDifference throughput | records/s served | replay reads at varied limit (1, 256, 1000) from cold (mmap fault) and warm (page cache) segments; with and without deleted/node-skipped records in the scanned range. |
| getDifference latency p50/p99 | per-call latency | tail reads (caught-up, near head) vs deep replay (cold segments). |
| SSE fan-out latency | write→deliver p50/p99 | 1 writer, N watchers (1, 10, 100, 1000) on one topic; measure per-watcher delivery latency; verify the 1–5 ms target for eventual. |
| SSE fan-out scale | max watchers / connection churn | connection setup cost, heartbeat overhead, memory per idle connection. |
| Router forwarding | added latency + throughput | src→dst delivery latency vs direct write; fan-out to N dests; chain depth cost. |
| Eviction / TTL cost | impact on write path | sustained writes against a small cap and short TTL; confirm segment-granular drop, measure write-latency impact and tombstone-emission rate. |
| Recovery time | time to ready after restart | WAL replay + segment .idx load for topics holding 10⁶ / 10⁷ / 10⁸ records; report seconds-to-ready vs data size. |
| Throttling behavior | latency under pressure | drive CPU saturation; chart high- vs low-priority topic latency and 429 rate; assert zero loss. |
| Memory footprint | bytes/record resident | index + buffers per retained record at varied payload sizes; confirm the base+offset index overhead. |
Baseline doc: docs/BENCHMARKS.md (created in phase 3). It records the phase-2/3 in-memory
numbers first; phase 4 appends a persistent-build column and a short analysis of the durability and
recovery costs.
- Tombstone placement in
diff: chosen — a dedicated top-leveltombstonefield (not inline inrecords), so consumers branch cleanly. (Resolved; documented in API §3.3.) - Cursor epoch encoding: recommended yes — include an opaque
epochso delete+recreate is detected exactly rather than heuristically (DESIGN §5.5). Whether to expose the epoch innext_from_seq/SSEid:or keep it server-side is an implementation detail to settle in phase 4. - Delete un-delete: Resolved — deletion is permanent by design. Deleted records are
logically gone instantly but stay on disk just marked — there is no compaction and no
per-record reclaim (only a whole cleared segment is dropped) — and there is no un-delete in
/v0; to restore a value, write a new record. (This supersedes the earlier read-time-filter model, where removal was a reversible filter.) - Per-message explicit ack / lease / heartbeat (BullMQ stalled-job mode): Resolved —
implemented as the
queuetopic type (type:"queue"): claim/ack/nack/extend + the/workauto-claim SSE stream, visibility-timeout leases, redelivery, capped-redelivery → dead-letter, and optionallease_idfencing (validate-when-supplied). See API §10 / DESIGN §10. - Compacted topic type (Kafka log compaction, last-record-per-key): Out of scope — LSM / keyed
compaction is not implemented and not planned. The last-record-per-key pattern is built at the
application level with a tag + a point-in-time
matchdelete of prior versions. - Durable consumer groups as a server primitive: Out of scope — they are an application-level pattern (a topic per consumer + delete-as-ack), not a built-in server feature.
- Multi-server / replication / HA, native TLS, hard multi-tenancy: Out of scope — topics is single-server with an advisory data-dir single-writer lock (TLS terminates at a reverse proxy; tenancy is per-key scopes + topic-name-prefix allowlists).
- Auto-priority constants (
AUTO_MAX=500,HALF_LIFE_MS=30000) and band weights/boundaries are starting defaults; phase 3/4 benchmarks may retune them. The formula and knobs are stable; the numbers are tunable.