How it works
Three planes. Keep them separate or you couple storage to a machine again.
Control / data / compute
┌────────────┐ ┌─────────────┐ ┌──────────────┐
│ Client │────▶│ rubberduck │────▶│ D1 control │
└────────────┘ │ (gateway) │ └──────────────┘
│ │
│ │ DUCKLING DO stub
│ ▼
│ ┌──────────────────────┐ ┌─────────────┐
└───▶│ rubberduck-duckdb │────▶│ R2 Parquet │
│ Duckling + DuckDB-WASM │ │ org_*/… │
└──────────────────────┘ └─────────────┘Control plane (D1)
Orgs, ducklings, placement, query metrics. Tiny rows. This is where D1 belongs.
An org is the tenancy and placement boundary. A duckling is an isolated compute unit (Durable Object) with its own DuckDB-WASM engine — scales to zero when idle.
Data plane (R2)
Bucket: rubberduck-analytics
org_acme/
events/part-000.parquet
customers/part-000.parquetTable name = first directory under the org prefix. Catalog today is a directory listing, not Iceberg.
Durable bytes live here. Kill every Worker. Data remains.
Compute plane (DuckDB)
Worker rubberduck-duckdb hosts Duckling Durable Objects. Each duckling runs @ducklings/workers (DuckDB-WASM with Asyncify httpfs) inside its own isolate. Stock @duckdb/duckdb-wasm wants eval / dynamic compile. Workers refuse that. Ducklings ships a precompiled module.
On query:
- Auth + SQL policy on
rubberduck(gateway) - Resolve org prefix + duckling from D1
- Forward to that duckling's DO (
/query) - Duckling engine ensures an R2 secret, builds
VIEWs overread_parquet('r2://rubberduck-analytics/…')for tables named in the SQL - Run query, return JSON
Why two Workers: the WASM is ~41MB / ~10 MiB gzip. Stuffing it into the UI app exceeds Cloudflare's paid script limit. Engine lives on rubberduck-duckdb; gateway stays thin on rubberduck.
Query path, short
bash
curl -s -X POST https://rubberduck.b-christopher-3rd.workers.dev/v1/orgs/org_tpch/query \
-H 'authorization: Bearer demo-token' \
-H 'content-type: application/json' \
-d '{"sql":"SELECT count(*) FROM lineitem"}'
# 600572Warm groupbys on the sample orgs land around 150–300ms of engine wall time. Client latency runs higher — cold duckling isolates and view setup are part of the envelope. See Limits.
Outgrowing the Worker? See Scaling — same R2 prefix, bigger duckling.
Security (P0 → P1)
P0 uses a shared demo bearer. SQL policy blocks ATTACH/COPY/INSTALL and URI escapes. Views are scoped by org prefix. Per-org keys and scoped R2 credentials land in P1 for production multi-tenant use.