INSIDE ROCKBEE AN INTERACTIVE FIELD GUIDE

Follow one record.
Understand the system.

A small API has a lot happening underneath. Walk a request through Rockbee, move data between its views, and see exactly what shared object storage changes.

Start with a write
01 Durable writes02 Independent readers03 SQL over Parquet

01 / FOLLOW THE REQUEST

ILLUSTRATIVE WALKTHROUGH · NO LIVE CLUSTER

One API.
Different paths.

Pick an operation. Advance one step at a time to see who does the work, what gets stored, and when the client can call it done.

products / product-42
COMPUTE + SHARED STORAGE Current step
Rockbee request architectureThe client connects to the gateway. Writes go to one writer using SlateDB and object storage. Reads go to independent readers. The writer's background exporter publishes Parquet; a reader downloads it to its local cache for DuckDB SQL. The current step is also explained in adjacent text. DURABLE OBJECTS ClientRust SDK GatewayRoute + bound WriterSlateDB WAL + SSTsShared KV data ReaderSlateDB + cache ExporterInside writer DuckDBInside reader Parquet+ manifests TCP + PROTOBUF BACKGROUND PUBLICATION DOWNLOAD → LOCAL FILE CACHE
ClientRust SDK
GatewayRoutes requests
WriterDurable mutations
ReaderPoint reads + SQL
Shared KV dataSlateDB WAL + SST files in object storage
ExporterInside writer
DuckDBInside reader
Parquet + manifestsPublished SQL view in object storage
Solid: request / data accessDashed: asynchronous export
STEP 1 / 6

CLIENT

Give the product an ID.

The table handle uses the same bounded client connection as the key-value API. The document is a JSON object.

let products = client.table("products");
products.put_key("product-42", &json!({
    "name": "Trail Backpack", "price": 79
}), None).await?;

PUT success acknowledges SlateDB durability. Reader and SQL visibility follow separately.

Raw KV uses the same writer and reader routes. Only JSON table changes feed the Parquet projection.

The arrows follow real code: routing, durable writes, SQL execution.

02 / MAKE EVENTUAL CONSISTENCY VISIBLE

Stored. Readable. Queryable.
Three different moments.

Start with a published product at $79. Change it, refresh the reader, then publish SQL. Try deleting it before both views catch up.

01 / WRITERv1

Durable state

What the writer has acknowledged.

{"name":"Trail Backpack","price":79}
Durable
02 / POINT READv1

Reader’s view

What products.get(id) can see.

{"name":"Trail Backpack","price":79}
Caught up
03 / SQL QUERYv1

Published view

What the latest manifest exposes.

{"name":"Trail Backpack","price":79}
Published

All three views start at the same version. Write a new price to separate them.

This is a model, with the catalogue already discovered. Buttons stand in for background work; they are not public client methods. GET and SQL can catch up in either order.

Delete changes logical visibility. Previously published Parquet files remain in object storage; automatic cleanup is not currently implemented.

03 / THE ARCHITECTURAL CHOICE

Compute comes and goes.
Durable data stays.

SlateDB keeps the durable database in object storage. Rockbee readers open that shared data and build their own working caches. SQL adds a separate Parquet representation.

A

The authoritative KV store

SlateDB manages WAL files, sorted string tables, and metadata. The writer waits for durability before a successful mutation acknowledgement.

OPAQUE BYTES + JSON DOCUMENTS
B

The derived SQL view

The writer’s exporter turns a snapshot and later WAL changes into immutable Parquet. Manifests tell readers which files form a published table.

PARQUET SEGMENTS + MANIFESTS
C

The disposable working set

Each process has SlateDB caches. Each SQL reader also caches Parquet in local temporary files. Replacing a reader loses warmth, not authoritative data.

MEMORY CACHE + LOCAL QUERY FILES
ADD READER COMPUTEILLUSTRATION

More read traffic?
Add reader compute.

R1cacheR2cache
One shared object storeOne writer per prefix

Extra readers add serving resources and cache capacity. Capacity gains depend on the workload and shared bottlenecks.

What this buys you

Reader capacity can grow without provisioning a complete durable database copy per reader. You still pay for compute, caches, object requests, transfers, and cold-start work. Operators provision the readers; the system does not automatically scale them.

04 / WHERE OTHER DESIGNS GET EXPENSIVE

Every database
sends you a bill.

Sometimes it is RAM. Sometimes it is replica storage, rebalancing, or operational work. The useful question is which tradeoff your application can afford.

Redis OSS

Self-managed Redis Open Source with RDB / AOF persistence. Redis Cloud and Redis Software have different operating models.

RAM is a great place for a hot working set. Keeping an entire growing dataset there changes the economics.

WHERE IT CAN HURT

  1. Memory is a capacity decision. Keys, values, and data-structure overhead consume RAM. Redis recommends provisioning for peak memory; persistence does not turn the live dataset into an object-store working set.
  2. Durability has knobs. RDB snapshots can lose changes since the last snapshot. AOF improves recovery freshness, with log growth, rewrites, and replay to manage.
  3. Background work still costs resources. Snapshotting and AOF rewriting use fork and copy-on-write. Large, write-heavy datasets need memory headroom and latency monitoring.

WHY YOU MIGHT STILL CHOOSE IT

  • Rich in-memory data structures for counters, queues, sets, and hot application state.
  • Real persistence options: RDB, AOF, or both. Redis is much more than a disposable cache.

What Rockbee changes

Durable data lives in shared object storage; readers cache their working sets. The cost is eventual reader visibility and cold object fetches. Rockbee does not provide Redis’s data structures or promise its in-memory latency.

Couchbase Server

Self-managed Couchbase Server, including Enterprise Magma. Capella and other managed Couchbase offerings are separate comparisons.

A broad distributed database gives you more capabilities—and more service placement and capacity decisions.

WHERE IT CAN HURT

  1. The service topology is yours to manage. Data, Query, Index, Search, and other services have their own placement and resource needs. A SQL++ query can involve several roles.
  2. Growth can involve rebalancing. Adding or removing nodes and changing service placement can redistribute data and indexes. Capacity changes are cluster operations to plan and monitor.
  3. Disk-oriented still needs sizing. Magma supports datasets larger than memory, but storage hardware, memory quotas, flushing, and compaction remain part of the operating model.

WHY YOU MIGHT STILL CHOOSE IT

  • Multi-Dimensional Scaling already separates services so Data, Query, Index, and Search can scale independently.
  • SQL++, indexing, search, and a disk-oriented storage engine provide a broader document-database toolkit.

What Rockbee changes

A smaller topology: one writer, independent readers, and shared durable objects. You give up Couchbase’s broader indexing and service ecosystem. Rockbee’s SELECT-over-Parquet path is a different query contract.

PostgreSQL

PostgreSQL 18 with conventional physical primary / standby replication. Managed services and shared-storage variants can differ.

Read replicas bring more serving capacity. They also bring database copies, WAL replay, and another recovery path.

WHERE IT CAN HURT

  1. A physical standby starts from a database copy. Each maintains its own data directory and applies the primary’s WAL. More replicas mean more storage and replay work.
  2. Lag and durability are explicit choices. Asynchronous replicas can lag and lose recent commits on failover. Synchronous replication waits for selected standbys and adds commit latency.
  3. WAL retention needs attention. Slots protect lagging replicas, but retained WAL can fill storage. A replica that falls behind available history can require a new base backup.

WHY YOU MIGHT STILL CHOOSE IT

  • ACID transactions, constraints, indexes, joins, and a mature relational SQL ecosystem.
  • With configured synchronous standbys, synchronous_commit = remote_apply waits until changes are applied and visible there.

What Rockbee changes

Readers use shared durable objects instead of maintaining a complete durable replica. Rockbee trades away multi-key transactions, immediate reader freshness, and automatic writer failover. A transactional system of record may still belong in PostgreSQL.

Rockbee’s bet: durable data belongs in shared storage.

Choose it when byte-key lookups, flexible JSON, and an eventually published SQL view fit the workload. A single writer, eventual reads, and bounded single-table queries are part of that choice. Checkout transactions and strict read-after-write requirements call for a different contract.

Architecture comparison, reviewed 18 October 2026. Product scopes and official documentation are linked above. These are qualitative tradeoffs, not a performance benchmark or a price comparison.

05 / UNDER THE HOOD

Open the details.
Follow the code.

The API, protocol, storage layout, publication rules, and failure paths. Implementation links are pinned to the revision reviewed for this guide.

The public client: KV by default, tables when you need them

RockBeeClient exposes put, get, and delete for nonempty opaque byte keys and values. User keys are limited to 65,534 bytes; the complete wire frame must also fit its 1 MiB bound. client.table("products") creates a JSON-table handle sharing the same connection, admission limits, and shutdown lifecycle. It uses composition, not inheritance.

use rockbee_client::RockBeeClient;
use serde_json::json;

let client = RockBeeClient::connect("127.0.0.1:7100").await?;

// Byte values: the application chooses the encoding.
client.put(b"cart:customer-42", b"product-42:2", None).await?;
let cart = client.get(b"cart:customer-42").await?;

// JSON table: generate a UUID, or choose your own ID.
let products = client.table("products");
let id = products.put(&json!({
    "name": "Trail Backpack", "price": 79
})).await?;
let product = products.get(&id).await?;
// Either GET can lag a preceding durable PUT.

put_key(id, document, ttl) replaces a document under a supplied ID. JSON objects can have different fields. Raw keys and table IDs live in separate physical namespaces.

KV API · Table API

What a successful PUT means, and what a lost reply means

A PUT starts eagerly and returns a completion handle. Dropping an admitted handle does not cancel the write. Awaiting success confirms that the writer acknowledged SlateDB durability. Shutdown drains admitted operations, including dropped handles, before disconnecting.

OutcomeWhat the application knows
NotSentThe client did not enqueue the request.
RejectedA server response confirms the mutation was not applied.
UnknownThe mutation may have become durable. A lost reply cannot settle that question.

The client and gateway do not automatically replay uncertain mutations. Request IDs correlate responses on a connection; they are not durable deduplication keys. By default, client clones share 128 active and 128 pending slots, with a 5-second end-to-end request deadline.

Client contract · Mutation outcomes

On the wire: Protobuf messages over bounded TCP connections

proto/rockbee.proto defines the versioned envelope, requests, responses, and correlation IDs. Prost generates Rust types; the generated file is checked into the repository and included by the protocol crate. Ordinary Cargo builds use that file. Regeneration is an explicit developer command.

4-byte big-endian length | Protobuf Envelope
                          version = 1
                          request_id = positive integer
                          oneof: KV / Table / Status / Response

Maximum frame: 1 MiB, including the length prefix.

This is a custom TCP protocol. Multiplexed requests use positive IDs; zero is reserved for sequential mode. The shared RPC layer bounds connections and in-flight work. Its frame read/write timeouts are separate from client and query execution deadlines.

cargo run -p rockbee-protocol --example generate_protocol

Wire schema · Code generation

Gateway routing, reader refresh, and caches

Validated mutations take the one writer route exactly once. GET and SQL requests go to readers in round-robin order, with reader failover inside one routing budget. A successful missing-value response is final; the gateway does not ask another reader to search for a newer value.

Readers use SlateDB’s read-only DbReader, replay WAL data, and refresh their view. The checked-in poll interval is 10 seconds. That is a polling setting, not a visibility guarantee. Lower intervals trade extra object-store requests for more frequent refresh.

The gateway has no separate KV cache. Each writer or reader has SlateDB block and metadata caches; SQL adds a reader-local Parquet file cache. Cold requests can fetch durable objects, while warm requests may use the working set already present.

Router · Reader · Reader configuration

A table is a separate JSON namespace, not a different database

Rockbee keeps opaque byte keys and JSON tables in the same SlateDB database, under disjoint physical key prefixes. A table name is an ASCII identifier of 1–64 bytes; a document ID is UTF-8 and may be up to 1,024 bytes. A table value must be a JSON object. A table PUT writes the document and a durable catalogue entry in one SlateDB batch, so the exporter can discover the table after a restart.

raw key       = [0, ...]
table prefix  = [1, table_name_length, ...table_name_bytes]
catalogue key = [2, ...table_name_bytes]

Export objects are deliberately outside the SlateDB data paths. The table name is hex encoded, preserving distinct case-sensitive names even when the object store is a local filesystem.

<prefix>/_rockbee_tables/<hex-table-name>/
  segments/<uuid>.parquet
  manifests/<20-digit-revision>.json

table limits · table key encoding · durable table PUT · object layout

Export starts with a pinned snapshot, then follows the WAL

A writer-owned background worker checks registered tables every 500 ms. A table’s first export, a compaction, or a detected WAL gap builds a new base generation. This work is independent of the mutation acknowledgement: the client receives success only after SlateDB durability, not after Parquet publication.

  1. Flush the writer memtable and create a detached SlateDB checkpoint with a 24-hour lease.
  2. Scan that pinned checkpoint for the current rows of one table, writing bounded Parquet batches.
  3. Record the checkpoint’s WAL high-water mark, then consume the committed WAL tail through EOF.
  4. Publish the complete generation only after all of its segments exist.
Export boundValue
Batch before encoding1,000 rows or approximately 8 MiB
Encoded Parquet segment32 MiB maximum
Snapshot checkpoint lease24 hours

The manifest cursor is exported_wal_file_id. It advances only after a whole WAL file has reached EOF; uploaded partial batches are not made visible on their own. A missing/truncated WAL causes a fresh pinned snapshot rather than skipping history.

export constants · worker lifecycle · snapshot and tail reconciliation · WAL cursor rule

Immutable segments become visible through a manifest revision

Each Parquet row contains the document ID and JSON value plus the ordering and visibility metadata needed to reconstruct current table state: seq, absolute expiry, tombstone, WAL file ID, and row ordinal. Segment writes use create-only object-store operations. Only afterwards can the worker publish a new create-only manifest revision.

ILLUSTRATIVE MANIFEST EXCERPT · OTHER FIELDS OMITTED
{
  "format_version": 1,
  "revision": 42,
  "writer_epoch": 9,
  "exported_wal_file_id": 1234,
  "segments": [{ "path": "…/segments/uuid.parquet", "bytes": 1048576 }]
}

Publication checks the active SlateDB writer epoch, requires the expected preceding revision, and rejects a cursor that would regress. The result is a complete immutable generation or the preceding published generation; a failed upload or publication does not make partial state queryable.

manifest schema · segment upload and publication · writer-epoch fence

SQL reads local cached Parquet, then reconstructs the latest document

A reader reloads the table manifest for every SQL query. It checks that every referenced segment stays inside that table’s object prefix, downloads any cache miss into a reader-owned temporary directory, verifies its manifest size, and gives DuckDB those local file paths. DuckDB does not directly read S3 or SlateDB for a query.

Before your SQL predicates run, Rockbee builds the selected table view by selecting the newest row for each id. The ordering is exact:

row_number() OVER (
  PARTITION BY id
  ORDER BY seq DESC, wal_file_id DESC, row_ordinal DESC
)

Only rank 1 remains; then its tombstone and expiry are evaluated. This ordering matters: an older live value cannot reappear when the newest record is a delete or has expired.

manifest reload and execution · local download cache · deduplication and visibility view

Deletes and TTL use the same newest-record rule

A table delete is a durable SlateDB tombstone. The exporter preserves it as a Parquet row with tombstone: true; queries remove it only after deciding that it is the newest row for that ID. This keeps a historical pre-delete value from resurfacing.

TTL is stored as an absolute Unix-millisecond expiry. ttl_millis = 0 means no expiry. At the exact expiry instant a point read is invisible, and query SQL uses the equivalent condition expires_at_unix_ms > now. A later non-expiring PUT replaces an earlier expiry because it is the newest record.

ttl_millis == 0  // Ttl::NoExpiry
ttl_millis > 0   // Ttl::ExpireAtMillis(now + ttl_millis)

table TTL and delete durability · wall-clock TTL semantics · tombstone export

Query execution is deliberately bounded and single-table

Each query gets an in-memory DuckDB connection and a private spill directory. External access, persistent secrets, and extension auto-loading are disabled. SQL must be one SELECT; DuckDB parses it into a syntax tree, and Rockbee permits only the selected unqualified table and lexical CTEs. Expressions, joins over CTEs, and aggregates still run in DuckDB. Allowed table functions are range, generate_series, unnest, json_each, and json_tree; file scans, other tables, and catalogue access are rejected.

Per reader / per queryDefault bound
SQL text64 KiB
Concurrent SQL workers2; excess returns busy
Whole deadline, including downloads4 seconds
DuckDB memory / spill256 MB / 1 GiB
Result1,000 rows and 512 KiB JSON
Local Parquet cache8 GiB, 16,384 files, 32 MiB/object

The reader configuration can change timeout, DuckDB memory (1–16,384 MB), and concurrency (1–128). It does not make query state durable or turn the cache into an authority.

reader defaults and configurable limits · DuckDB isolation and result bounds · SQL source restrictions

Recovery favors a known published generation over a speculative one

On writer restart, Rockbee detects registered tables and restarts export even without another mutation. The exporter retries transient object-store failures, but a newer SlateDB writer epoch fences the old exporter. On normal shutdown it cancels and joins the exporter before closing the database; owned checkpoint cleanup is attempted on completion, failure, and cancellation.

Compaction creates a new snapshot generation after at least 128 delta segments and 60 seconds. It replaces what the current manifest references, preserving the same visibility rules. Manifest revisions and unreferenced Parquet objects are currently retained; automatic garbage collection is not implemented.

SQL is eventually consistent with durable writes. A registered table with no first manifest returns a warming error. A lagging reader may not yet see the catalogue entry and can treat the table as empty. The returned exported_wal_file_id describes the export generation used; it is not a write receipt or a cross-reader read barrier.

restart resumes export · writer shutdown · compaction and WAL-gap recovery · warming behavior

What SQL returns to the application

The selected table exposes id VARCHAR and value JSON. Your SELECT shapes each result row. SQL reads the published projection, so this example result applies after the product is visible in that generation.

let result = products.query(
    "SELECT id, value->>'name' AS name,
            TRY_CAST(value->>'price' AS DOUBLE) AS price
     FROM products
     WHERE TRY_CAST(value->>'price' AS DOUBLE) < 100
     ORDER BY price"
).await?;

for row in result.rows {
    println!("{row}");
}
EXAMPLE ROW · AFTER PUBLICATION
{"id":"product-42","name":"Trail Backpack","price":79.0}

The client returns JSON row values plus exported_wal_file_id. Results are bounded and materialized; this API does not return a streaming database cursor. The watermark describes the exported view, not confirmation that a particular recent write is present.

Query client · Result representation

Configuration: storage, serving bounds, and SQL resources

Writer and reader processes share the same backend, bucket or directory, and prefix. Current backend constructors cover S3, LocalStack, and local files. Native GCS and Azure backend configuration is not implemented. The gateway is configured with one writer and a list of readers.

# Reader-side SQL configuration
[query]
timeout_ms = 4000
memory_limit_mb = 256
max_concurrent = 2

[reader]
poll_interval_ms = 10000
Checked-in settingValue
RPC connections / global in-flight256 / 128 per process
RPC frame read / write timeout30 s / 5 s
SlateDB block / metadata cache512 MiB / 128 MiB per process
Reader replay memtable maximum64 MiB per memtable
Writer unflushed-data backpressure1 GiB

Cache capacities are not preallocated memory or total process RSS ceilings. Query memory is a DuckDB setting per query; concurrent queries and other process work consume additional resources.

Reader config · Writer config · Backend construction

CDC, restarts, and operating the processes

rockbee-cdc tails durable SlateDB WAL changes into JSON lines. It flushes output before advancing a locked, source-bound cursor. Crashes may repeat events, so consumers need at-least-once handling. TTL expiry does not emit a delete event; engine-private table and catalogue keys are filtered from this raw KV stream.

make local starts an owned writer, reader, and gateway on dynamic ports with a unique storage prefix. It prints connection details. Fixed development configs use gateway 7100, writer 7101, and reader 7102. Start the writer before the reader.

--check-config reports effective configuration without proving connectivity. A status probe reports endpoint role and local RPC counters; it does not prove storage health or reader freshness.

Reader request failover is implemented. Automatic writer failover and write sharding are not. The public transport is plain TCP without built-in authentication or TLS; deployments must supply the required network boundary. SQL path restrictions are not a tenant-authentication system.

CDC contract · Development & operations

The module map and the evidence behind this guide
ModuleOwns
rockbee-clientPublic Rust API, eager completion handles, admission lifecycle
rockbee-protocolProtobuf types, framing, validation, mutation certainty
rockbee-rpcBounded TCP server and response handling
rockbee-runtimeBackend construction, shared config and memory settings
rockbee-queryWriter export, manifest publication, reader SQL
rockbee-gateway / writer / readerSeparately runnable serving processes
rockbee-cli / cdcLocal workflow, requests, probes, WAL consumption

The repository contains durability, lost-acknowledgement, reader convergence, SQL update/delete/expiry, and publication recovery tests. Local fixtures establish those behaviors under their test conditions; they do not establish production throughput or linear scaling.

Storage behavior tests · Query tests · Gateway failure tests

NOW YOU KNOW THE PATH

Start small.
Keep the architecture clear.

Run Rockbee locally ↗Read the source ↗