Skip to content

rubberduck docs

Scales to zero · 25× faster groupbys · stay on Cloudflare

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.parquet

Table 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:

  1. Auth + SQL policy on rubberduck (gateway)
  2. Resolve org prefix + duckling from D1
  3. Forward to that duckling's DO (/query)
  4. Duckling engine ensures an R2 secret, builds VIEWs over read_parquet('r2://rubberduck-analytics/…') for tables named in the SQL
  5. 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"}'
# 600572

Warm 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.

Scales to zero. 25× faster than D1 on groupbys. No need to go to another provider.