The post-v0.1.0 plan (roadmap-v0.2.md, milestones v0.2.0–
v0.5.0) has fully shipped: durability hardening, query-at-scale, TTL /
snapshot / on-demand compaction, scoped rotatable API keys, an embeddable Go
engine, and full API parity across all seven client SDKs. ScrivaDB is now a
correct, embeddable, feature-complete single node.
This document plans the next arc: making ScrivaDB operable, observable, and
survivable as a real service — and closing the gap between what the embedded
engine can do and what the network API exposes — on the road to a frozen
v1.0.0.
It is derived from a codebase review of 2026-07-03. Each item lists the
problem (with file:line evidence where it exists today), the approach,
proto/API impact, the files touched, tests, and an acceptance bar.
Items are sized S (≤½ day), M (1–2 days), L (3+ days).
Workflow: one PR per task (small related fixes may batch). CI green before merge; every feature updates
docs/,README.md,CHANGELOG.md, and ticks its box here. Large items (R1, N2, Q6) are candidate warden agents in isolated worktrees.
| Milestone | Theme | Items | Risk | Headline |
|---|---|---|---|---|
| v0.6.0 | Operability & observability | O1–O5 | Low | Run it as a service — health, structured logs, tracing, backpressure |
| v0.7.0 | Network/engine API parity + query breadth | N1–N4 | Medium | The wire API is as capable as the embedded one; return only what you ask for |
| v0.8.0 | Replication & high availability | R1–R3 | High | Survive a node loss; scale reads to followers |
| v0.9.0 | Security & tenancy depth → 1.0 readiness | S1–S4 | Medium | mTLS, audit, per-collection ACLs, quotas; then freeze for v1.0.0 |
The critical, differentiating arc is v0.8.0 (replication) — everything before it de-risks operating a fleet; everything after it hardens the multi-tenant security story before the API is frozen at 1.0.
Today the server is a black box to operators. It logs with the stdlib log
package from exactly one place (cmd/scriva/main.go:7), exposes no
health/readiness probe and no gRPC health service, has no tracing, and
applies no backpressure — a single client can open unbounded concurrent
streams. These are the table-stakes for running ScrivaDB behind a load balancer or
in Kubernetes.
log in cmd/scriva/main.go; the
engine and gRPC layers log nothing. There is no request log, no level control,
no machine-parseable output.log/slog (stdlib, zero new deps). A configured
*slog.Logger (JSON or text handler, level from config) is threaded through
server and injected into the engine via the existing hook pattern in
engine.CollectionConfig (never import a logger into the engine directly —
same rule as metrics). Add a unary/stream interceptor that logs each RPC
(method, principal, duration, status) at debug/info.--log-level and --log-format (Config + YAML + flag, per the
CLAUDE.md flag checklist).server/logging.go (new), server/grpc.go, server/config.go,
cmd/scriva/main.go, internal/metrics/-style hook wiring.make deps-check green).grpc.health.v1.Health service and no HTTP /healthz
/readyz. Orchestrators and LBs cannot probe liveness/readiness.google.golang.org/grpc/health service and
mark SERVING once listeners are up. Add GET /healthz (process alive) and
GET /readyz (DB open, data dir writable) to the REST mux. Flip to
NOT_SERVING during graceful shutdown so in-flight drains cleanly.scriva.proto); two REST
routes registered directly on the gateway mux.server/health.go (new), server/rest.go, cmd/scriva/main.go./readyz fails when the data dir is unwritable.server/grpc.go); a greedy or buggy client can exhaust goroutines/FDs. No
per-key rate limiting despite scoped keys existing.grpc.MaxConcurrentStreams and a server-wide in-flight
semaphore interceptor returning RESOURCE_EXHAUSTED when saturated; (b)
optional token-bucket rate limit keyed by API-key principal (reuse the auth
interceptor's resolved principal). Both off by default, opt-in via flags.--max-concurrent-streams, --max-inflight, --rate-limit
(per-key rps).server/limits.go (new), server/grpc.go, internal/auth/
(expose principal to the limiter), server/config.go, cmd/scriva/main.go.RESOURCE_EXHAUSTED;
rate limiter throttles one key without affecting another.Find can't be attributed across
the gateway → gRPC → engine hops.--otlp-endpoint is set) that spans each RPC; add engine-level span hooks
(via the config hook pattern) around scan/compaction so slow segments are
visible. Keep the engine package dependency-free — the engine emits timing
through the existing hook, the server owns the OTel SDK.--otlp-endpoint, --otlp-sample-ratio.server/tracing.go (new), server/grpc.go, cmd/scriva/main.go,
engine hook call sites.Find produces a trace
spanning gateway → gRPC → engine scan; disabled by default.CollectionStats reports records/segments/dirty/size but nothing
about query cost; there's no way to find pathological scans.Find/Scan exceeding a configurable
threshold with filter shape, rows scanned vs returned, and whether an index
was used (the planner in engine/scan.go already knows). Surface
rows_scanned on the metrics histogram.--slow-query-ms.engine/scan.go (return scan stats), server/grpc.go,
internal/metrics/metrics.go.The embedded engine is materially more capable than the wire API. Keyed CRUD
(InsertWithKey/FindByKey/…), Upsert, compare-and-swap (UpdateIfRev /
UpdateIfMatch), per-record Rev, and Count/Exists all exist in the engine
(shipped in v0.2.0, see CHANGELOG.md) but have no gRPC/REST surface. Network
clients also can't project fields, aggregate, sort by more than one field, or
paginate without O(offset) cost. This milestone closes those gaps — mostly
exposing proven engine capability, which keeps risk moderate.
uint64 ids and last-writer-wins
updates — no optimistic concurrency, no natural keys, no upsert.Upsert, FindByKey, UpdateByKey, DeleteByKey,
UpdateIfRev, and include key/rev on Insert/Find/Update responses. Map
straight onto the existing engine methods; return the typed engine errors
(ErrDuplicateKey, ErrKeyNotFound) as gRPC status codes
(AlreadyExists/NotFound).proto/scriva.proto; add string key
and uint64 rev to record-bearing responses. Regenerate stubs.proto/scriva.proto, server/grpc.go, cmd/scriva-cli/, docs; and
a follow-on parity pass across the 7 SDKs (one PR each, as with the TTL sweep).grpc_integration_test.go — upsert insert-then-replace; CAS with a
stale rev fails cleanly; duplicate key → AlreadyExists.fields projection to FindRequest /
FindByIdRequest; the engine filters the decoded data map to the requested
keys before it hits the wire (id/key/rev always included). Empty = full record
(backward compatible).repeated string fields on the read requests.proto/scriva.proto, engine/scan.go, server/grpc.go, CLI
--fields, docs, SDK follow-on.FindRequest.offset,
proto/scriva.proto:242) — O(offset) to skip — and order_by is a single
field (engine/scan.go:28). Deep pagination and tie-broken ordering are
expensive/ambiguous.page_token (encodes the last (sort-key, id) seen)
so the engine can seek past already-returned rows instead of counting; support
a repeated/typed order_by with per-field direction, reusing the typed
query.Compare. Offset stays for compatibility.string page_token on request/response; repeated OrderBy {field, desc} (deprecate the scalar order_by/order_dir with a migration
note — a legitimate minor-bump break per the semver policy).proto/scriva.proto, engine/scan.go, server/grpc.go, CLI, docs,
SDK follow-on.Count exists in the engine but not on the wire, and there is no
group-by or numeric aggregation — clients pull whole collections to count.Aggregate RPC: count, and group_by(field) yielding
per-group count/sum/avg/min/max over a numeric field, honoring the
same Filter. Streams groups. Uses indexes for grouped-eq where available.Aggregate RPC + request/response messages.proto/scriva.proto, engine/aggregate.go (new), server/grpc.go,
CLI, docs, SDK follow-on.Find length; group-by sums equal a manual reduction;
filter is honored.count/group-by return correct results without materializing
the collection client-side.The flagship arc. ScrivaDB is single-node: a lost disk or crashed process is an outage and a potential data loss beyond the last snapshot. The append-only segment design is already a write-ahead log, which makes leader→follower log shipping the natural HA primitive.
Replicate RPC that ships committed segment entries (post-fsync) with a
monotonic global sequence number; the follower applies them through the normal
write path (index + secondary indexes maintained identically) and tracks its
applied LSN. Bootstrap a fresh follower from a Snapshot (already exists) then
catch up via the stream. Async replication first (bounded lag), with an
acknowledged-LSN channel to enable R2.Replicate streaming RPC; a ReplicationStatus RPC
(leader LSN, per-follower applied LSN, lag).engine/replication.go (new), engine/collection.go (emit
committed entries with LSN — reuse the Watch fan-out plumbing),
proto/scriva.proto, server/grpc.go, cmd/scriva/main.go (--replicate-from
follower mode), docs.FailedPrecondition
("read-only replica; write to the leader"), optionally reporting its LSN lag so
clients can bound staleness.ReplicationStatus.server/grpc.go (role-aware routing), cmd/scriva/main.go, docs,
optional SDK helper for "read from any replica".Promote RPC (and CLI) that flips a caught-up follower
to leader (stops replicating, accepts writes) with a guard that refuses to
promote a lagging replica beyond a threshold. Document the operator runbook;
leave automatic leader election (consensus) explicitly out of scope for now.Promote admin RPC (scoped to an admin key from A1/S3).engine/replication.go, server/grpc.go, cmd/scriva-cli/, docs
(docs/operations.md).The last hardening before freezing the API. Scoped rotatable keys shipped in v0.5.0 (A1); this milestone rounds out transport auth, auditability, finer-grained authorization, and resource fairness, then freezes the surface.
internal/auth/, server/grpc.go, cmd/scriva/main.go, docs.--audit-log path.server/audit.go (new), server/grpc.go, config/flags, docs.internal/auth/, server/config.go, docs.PermissionDenied) elsewhere; reload picks up ACL changes.ResourceExhausted; surface usage via
stats/metrics.engine/collection.go (write-path check via a config hook),
server/config.go, internal/metrics/, docs.CHANGELOG.md policy).v1.0.0.v0.6.0 PR-A: O1 (slog) + O2 (health) (do first — everything else logs/probes)
PR-B: O3 (backpressure)
PR-C: O4 (tracing) , PR-D: O5 (slow-query log)
v0.7.0 Agent-1: N1 (keyed/CAS/upsert wire) + 7-SDK parity follow-on
PR-E: N2 (projection)
PR-F: N3 (cursor pagination + multi-sort)
Agent-2: N4 (aggregations)
v0.8.0 Agent-3: R1 (replication — flagship)
PR-G: R2 (read replicas) , PR-H: R3 (failover)
v0.9.0 PR-I: S1 (mTLS) , PR-J: S2 (audit) , PR-K: S3 (ACLs) , PR-L: S4 (quotas)
then v1.0.0 freeze
Each large item (N1, N4, R1) is a candidate warden development agent in its own
worktree; the rest ship as direct PRs. Every merged item ticks its box below and
updates ROADMAP.md, docs/, README.md, and CHANGELOG.md per CLAUDE.md.
v0.6.0 — Operability & observability
slog)v0.7.0 — Network/engine API parity + query breadth
v0.8.0 — Replication & HA
v0.9.0 — Security & tenancy depth
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |