Simple by key.
Powerful
with SQL.
Store application data by key. Query JSON with SQL. Keep durable data in object storage, and scale your readers as your application grows.
Rust client. SlateDB persistence. DuckDB queries.
01 / THE INTERFACE
Look up by key.
Explore with SQL.
Product catalogs, application content, or saved model outputs. Use one client to fetch a record by key and query JSON fields with SQL.
Your fields.
Your next idea.
Put a JSON object. Get a UUID back. Add different fields to the next document, or use your own ID for a replacement.
- Nested objects and arrays
- Generated or caller-supplied IDs
- Get, replace, delete, and optional TTL
let products = client.table("products");
let write = products.put(&json!({
"name": "Keyboard",
"price": 99.5,
"brand": "Logitech"
}));
let id = write.await?; // Durable acknowledgement.
let document = products.get(&id).await?;Keep their cart.
Welcome them back.
Save a customer's shopping cart under a key. Fetch it when they return, and remove it after checkout. A TTL expires abandoned carts automatically.
- Key:
cart:customer-42 - Value: the cart encoded as JSON bytes
- Expires 30 minutes after this write
{
"items": [
{"sku": "keyboard-01", "quantity": 2}
]
}let cart_key = b"cart:customer-42";
// Save the cart for 30 minutes.
client.put(
cart_key,
br#"{"items":[{"sku":"keyboard-01","quantity":2}]}"#,
Some(std::time::Duration::from_secs(30 * 60)),
).await?;
// Load it when the customer returns.
// A recent write may not be visible to the reader yet.
let cart_bytes = client.get(cart_key).await?;
// After checkout is complete, remove the saved cart.
client.delete(cart_key).await?;None when no cart is visible. Your application decodes the JSON. PUT and DELETE await durable acknowledgements; reader visibility is eventual.From a document
to an answer.
Start with a JSON product. Fetch the document by ID, or ask a question across the table with SQL.
- Put the document.
putstarts the write and generates a UUID. Awaiting its handle returns that ID after a durable acknowledgement. - Get it by ID.
get(&id)returns the original JSON object from a reader. A new write can returnNoneuntil that reader catches up. - Query the table.
query(sql)runs your SELECT in DuckDB on a reader. This example finds products under 100 and returns onlynameandprice, ordered by price.
{
"name": "Keyboard",
"price": 99.5,
"brand": "Logitech"
}The SELECT aliases become JSON keys. brand stays in the stored document; this query does not select it. Other matching products produce more rows.
use serde_json::json;
let products = client.table("products");
// 1. Write JSON. Await a durable acknowledgement.
let write = products.put(&json!({
"name": "Keyboard",
"price": 99.5,
"brand": "Logitech"
}));
let id = write.await?;
// 2. Read the original document by ID.
// May be None until the reader catches up.
let document = products.get(&id).await?;
// 3. Query the published table with real SQL.
// The latest write may not be published yet.
let result = products.query(
"SELECT 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?;
println!("{}", serde_json::to_string(&result.rows)?);[
{
"name": "Keyboard",
"price": 99.5
}
]value holds the stored JSON. ->> extracts a field as text; TRY_CAST converts price to a number for filtering and sorting. Each selected row becomes a JSON object in result.rows.The writer publishes Parquet in the background. A query made immediately after PUT can see older data or report that the table is warming up. If you need this new record, retry the read/query with a deadline; never replay an uncertain write. The full SDK example shows bounded polling.
Connect once with RockBeeClient::connect(addr).await?
02 / ROOM TO GROW
More readers.
Same durable dataset.
Read demand and data size are different things. Provision reader compute separately, without a full durable copy of the dataset for each reader.
MinIO or your S3-compatible storage. Connect through the S3 backend. Local development also supports LocalStack and local files. Validate your chosen provider with your workload.
One writer, one reader. Separate processes, shared durable data.
Illustrative topology · readers are configured explicitlySize compute for the work.
Choose writer and reader resources independently. Add read-serving processes as your workload needs them.
Keep durable data in place.
A new reader opens the shared dataset and builds its local cache. Durable storage stays separate from the process.
Replace a process. Keep going.
Reader compute is replaceable. Metadata loading, catch-up, and cache warmup still take time.
03 / THE FOUNDATION
Independent compute.
Shared durable storage.
Compute serves requests. Object storage keeps durable data. This is disaggregated architecture: size readers for traffic and storage for the dataset, without a full durable database copy for every reader.
- 01 Gateway
- Your client connects here. It sends PUT and DELETE to the writer, and GET and SQL to readers.
- 02 One writer
- Owns mutations for a storage prefix. SlateDB makes writes durable. The writer also uses DuckDB to export JSON table data into Parquet in the background.
- 03 Independent readers
- Serve lookups and queries with their own compute and local caches. Add reader processes as read demand grows.
- 04 Object storage
- Holds shared durable files: SlateDB write logs and data files, plus the Parquet projection used for SQL.
Writer prepares the data.
The writer's export worker uses DuckDB to turn table data into Parquet files and publishes them in object storage. This runs separately from the durable PUT acknowledgement.
Reader answers the question.
The gateway sends your SELECT to a reader. Its DuckDB engine reads the published Parquet projection and returns the selected columns. It does not wait for a fresh export on each query.
Client → gateway → writer. SlateDB persists the mutation in object storage before the writer acknowledges success. Readers catch up independently; the writer publishes the SQL projection in the background.
More read traffic? Add reader compute.
Configure another reader against the same storage prefix and include it in the gateway's reader list. It opens the shared dataset and warms its own cache. You do not need a full durable replica for every reader, and adding a reader does not require increasing writer compute.
Reader provisioning is explicit. Catch-up and cache warmup take time; capacity depends on your workload and the rest of the system.
SlateDB serves the key.
A reader looks up the byte key or document ID through SlateDB. Its local cache keeps frequently used data close; shared object storage supplies the durable files. Readers follow the writer's updates, so a recent write may not be visible immediately.
Explore the readerWriter publishes. Reader queries.
DuckDB has a job on both sides. The writer uses it to create Parquet files from JSON table data and publishes those files asynchronously. On a reader, DuckDB runs your SELECT over the published projection and returns JSON rows. The export and the query are separate operations.
Explore the query engine04 / AI APPLICATIONS
Keep the context.
Query the results.
Your models produce more than answers. Store their outputs, saved context, evaluations, and chat or agent logs. Retrieve by key. Explore with SQL.
Generate once.
Reuse what fits.
A product description. A document summary. A classification. Save a completed output under a key that identifies the model version and input.
When the same result is useful again, your application can fetch it instead of making another model call.
- WRITE
- Your pipeline stores the completed result.
- READ
- Your application looks up its model-and-input key.
catalog-v3:product-42:r7{
"model_version": "catalog-v3",
"input_id": "product-42",
"input_revision": 7,
"description": "A compact wireless mouse.",
"tags": ["wireless", "office"],
"language": "en"
}
let outputs = client.table("model_outputs");
let output = outputs.get("catalog-v3:product-42:r7").await?;Include input and model versions in your key so the application can choose the right result.
Save the useful parts.
Bring them back.
Your application chooses the summaries, document references, and tool results worth keeping from a conversation or task.
Save that context as a JSON snapshot. A later request or worker can retrieve it by ID and use it to assemble the next prompt.
- CHOOSE
- Your application decides what belongs in context.
- RECALL
- Fetch the saved snapshot by conversation or task ID.
conversation-42:summary-v3{
"conversation_id": "conversation-42",
"summary": "Looking for a quiet wireless keyboard.",
"preferences": {
"layout": "US",
"budget": 100
},
"product_ids": ["keyboard-17", "keyboard-28"]
}
let context = client.table("agent_context");
let snapshot = context.get("conversation-42:summary-v3").await?;Keep the snapshot in the active request when you need it immediately; later reads follow reader catch-up.
Keep every result.
Ask better questions.
Write evaluation records with the model version, dataset, score, and the fields that matter to your team. Different runs can carry different JSON fields.
After publication, use DuckDB SQL to compare model versions, filter a dataset, or aggregate scores across runs.
- RECORD
- Your evaluator writes a JSON document for each run.
- COMPARE
- SQL returns the selected columns as JSON rows.
SELECT
value->>'model_version' AS model_version,
COUNT(*) AS runs,
ROUND(AVG(TRY_CAST(
value->>'score' AS DOUBLE
)), 3) AS average_score
FROM evaluations
WHERE value->>'dataset' = 'support-v2'
GROUP BY 1
ORDER BY average_score DESC
| model_version | runs | average_score |
|---|---|---|
| assistant-v3 | 120 | 0.931 |
| assistant-v2 | 120 | 0.884 |
The writer publishes JSON table data as Parquet. Readers query that published view.
Keep the messages.
Follow the run.
Store chat messages, tool calls, and agent results as JSON events. Group them by conversation and run so the useful details stay connected.
Your application writes each event with its own ID and sequence. After publication, query a run to see what was said, which tools were used, and how they finished.
- CAPTURE
- Write the message or event your application produces.
- INSPECT
- Filter by run ID, event type, tool, or status with SQL.
conversation-42 / run-108- User message01
Find a quiet wireless keyboard under $100.
- Tool result02
184 ms · 2 matching productssearch_catalogok - Assistant message03
Two options match your preferences.
SELECT value->>'role' AS role,
value->>'event_type' AS event_type,
value->>'message' AS message,
value->>'tool' AS tool,
value->>'status' AS status
FROM agent_events
WHERE value->>'run_id' = 'run-108'
ORDER BY TRY_CAST(value->>'sequence' AS BIGINT)
LIMIT 200See a stored event
{
"event_id": "evt-108-02",
"conversation_id": "conversation-42",
"run_id": "run-108",
"sequence": 2,
"event_type": "tool_result",
"role": "tool",
"tool": "search_catalog",
"status": "ok",
"duration_ms": 184
}Each event is a JSON document keyed by event ID. Your application supplies the sequence; SQL reads the published view.
The same API.
More possibilities.
Prompt versions
Store prompt text and model settings under an explicit version ID. Your application selects the version to load for each request.
support-agent:v3Recommendations
Save product or content recommendations computed by your pipeline. Fetch a prepared list by user or segment ID while background jobs refresh it.
recommendations:user-42Feature serving
Keep pipeline-computed ML features under an entity-and-version key. Your application retrieves the prepared values when serving a prediction.
product-42:features-v2User feedback
Record ratings, comments, and the response or model version they refer to. Use SQL to inspect the published feedback and compare results.
response-108:feedback-1Your AI stack.
A shared data foundation.
Flexible records. Keep model-specific fields in JSON.
Durable object storage. Keep saved data independent of model workers.
Independent readers. Grow serving compute as more applications use the results.
05 / THE CONTRACT
Simple to use.
Clear about behavior.
- Durable writes.
- A successful mutation means SlateDB acknowledged durability. The client exposes uncertain outcomes and never automatically replays uncertain writes.
- Eventual reads.
- GET follows reader catch-up. SQL follows Parquet publication. A successful write can take time to appear on either read path.
- Bounded SQL.
- Reader query memory, concurrency, and timeouts are configurable. Results have explicit row and byte limits.
06 / BUILD SOMETHING
Take the small API
for a real spin.
Run a writer, a reader, and a gateway locally. Try byte lookups, write your first JSON document, then query the published table.
Open the setup guideRust via rustup + Docker Desktop.
See the guide for native DuckDB build setup on macOS.
git clone https://github.com/umit/rockbee.git
cd rockbee
# Requires Docker and port 4566 available.
docker compose up -d --wait
make localThe runner prints service addresses and uses a unique storage prefix. Keep it open; use its gateway address in your client.
Already running LocalStack? Follow the setup guide ↗A FEW GOOD QUESTIONS
Before you build.
Is this a key-value store or a SQL database?
The core stores opaque byte keys and values. JSON tables add document IDs and real DuckDB SELECT queries over a derived Parquet projection. SQL reads JSON tables; it does not interpret arbitrary byte values.
What does scaling mean here?
Reader processes share durable storage and can be provisioned separately from the writer. There is one writer per prefix. Operators configure readers; automatic scaling, sharding, and automatic writer failover are outside the current scope. Capacity depends on the workload, caches, gateway, network, and object store.
Why object storage?
Durable data lives independently of serving processes. Readers fetch shared files and cache their working sets locally, so each reader does not need a full independent durable replica. Cold reads, object requests, transfer costs, and cache warmup remain part of the design.
What should I evaluate first?
Start with a product catalog, application content, or saved model outputs where eventual reads fit the workflow. Run a writer and reader against your object store, then measure lookup latency, SQL publication lag, and recovery with your own data. The setup guide walks through the full path.
Small surface.
Room to build.