Quickstart: a governed DuckDB gateway in one command
Boot the gateway, connect a client, and run your first SQL query against a live TPC-H dataset.
Demo mode (self-contained, no Postgres)
The fastest way to try Quack on Demand end to end. demo boots a fully seeded instance against an embedded, ephemeral Postgres - so unlike every other path below, it needs no external Postgres and no Docker.
With uv installed there are no other prerequisites - the qod launcher downloads the release jar (verified against the published sha256), a Java runtime if none is on your machine (cached), and the ABI-pinned duckdb CLI, all under your user cache directory. The same command works on macOS, Linux, and Windows:
uvx qod@latest serve --demo
pip install qod && qod serve --demo is equivalent (qod start --demo still works as a deprecated alias). Two more routes to the same demo:
# Docker (trivial on Linux; on Mac/Windows requires Docker Desktop or a
# drop-in replacement like Podman, Colima, or OrbStack)
docker run --rm -p 20900:20900 -p 31338:31338 starlakeai/quack-on-demand demo
# from a source checkout or a downloaded assembly jar
# (needs a JDK and the duckdb CLI on PATH):
java -Darrow.allocation.manager.type=Unsafe \
-jar distrib/quack-on-demand-assembly-*.jar demo
It starts an embedded throwaway Postgres, seeds the minimal demo (tenant acme, tenant-db acme_tpch, schema tpch1) with a small TPC-H dataset, boots the manager REST API on :20900, the FlightSQL edge on :31338 (TLS on with an auto-generated self-signed cert; clients skip verification) and the native Quack front door on :9494 (what a DuckDB client ATTACHes to), and prints a connect snippet. All state lives under /tmp/qod-demo and is removed on exit (Ctrl-C).
Serve your own data (one command, no Postgres)
The demo is throwaway. To point the same gateway at data you already have, with nothing else to install:
uvx qod@latest serve ./sales.duckdb # an existing DuckDB file
uvx qod@latest serve ./warehouse/ # a directory of parquet / csv
uvx qod@latest serve s3://bucket/sales/ # a remote prefix
uvx qod@latest serve # a fresh, empty DuckLake to load into
QOD_ADMIN_PASSWORD='change-me' uvx qod@latest serve ./sales.duckdb # set the admin password up front, no prompt
# admin login: user admin; password admin with --demo, otherwise the one chosen on the first run
# (prompted, or QOD_ADMIN_PASSWORD; first run only, never stored; `uvx qod@latest admin reset-password` recovers it)
One command provisions a tenant, a database, and a pool around the target, then prints the JDBC / ADBC / ODBC strings. The control plane runs on a bundled persistent embedded Postgres under your user data dir: restart and everything is still there, and re-running adds a second database beside the first instead of replacing it.
Unlike --demo, this keeps the normal secure posture: TLS on, database auth on, ACL on, and an admin password you choose on the first run: prompted on a terminal, or taken from QOD_ADMIN_PASSWORD (without either, a non-interactive first run refuses to start). It is never stored, a later QOD_ADMIN_PASSWORD does not change it, and qod admin reset-password recovers it. An existing .duckdb file is attached read-write and served by a single node; parquet and CSV targets become views (read_parquet / read_csv), so nothing is copied or converted.
Which command do I want?
| Command | What it is | Needs |
|---|---|---|
qod serve --demo | throwaway showcase on sample data, insecure by design | nothing |
qod serve ./your-data | persistent gateway over your own data, secure defaults | nothing |
qod start | your deployment: your own Postgres, your config | Postgres + qod setup |
Demo admin console
Browse to http://localhost:20900/ui/ and log in with:
| Tenant ID | Username | Password | View |
|---|---|---|---|
| leave blank | root | demo-root | superuser console (all tenants) |
| leave blank | admin | admin, or your QOD_ADMIN_PASSWORD if exported | superuser console (all tenants) |
| leave blank | [email protected] | same as admin | superuser console (all tenants) |
acme | acme-admin | demo-acme-admin | acme-scoped roles, groups, users, pools |
acme | alice | demo-alice | acme-scoped view |
Demo SQL clients
All four users work with any Arrow Flight SQL client (JDBC, ADBC, ODBC) against localhost:31338, and with DuckDB itself against quack:localhost:9494, routed by tenant=acme and pool=bi. What each sees demonstrates row + column security, on either wire:
| Username | Password | Access |
|---|---|---|
alice | demo-alice | analyst: customer, orders, nation, region read-only; c_phone masked to ***; only c_mktsegment = 'BUILDING' rows; any other table denied |
acme-admin | demo-acme-admin | everything in acme, full and unmasked |
root | demo-root | superuser, bypasses per-statement ACL |
admin | admin | superuser, same as root |
JDBC (DBeaver, Spark, any JDBC tool; driver org.apache.arrow:flight-sql-jdbc-driver):
jdbc:arrow-flight-sql://localhost:31338?useEncryption=true&disableCertificateVerification=true&user=alice&password=demo-alice&tenant=acme&pool=bi
Swap user / password for any row above; for root or admin exclusively also append &superuser=true.
ADBC (Python, pip install adbc_driver_flightsql adbc_driver_manager):
from adbc_driver_flightsql import dbapi
conn = dbapi.connect("grpc+tls://localhost:31338",
db_kwargs={"username": "alice", "password": "demo-alice",
"adbc.flight.sql.client_option.tls_skip_verify": "true",
"adbc.flight.sql.rpc.call_header.tenant": "acme",
"adbc.flight.sql.rpc.call_header.pool": "bi",
"adbc.flight.sql.rpc.call_header.db": "acme_tpch"})
ODBC (any Arrow Flight SQL ODBC driver, e.g. the Dremio one; see ODBC for the odbc.ini form):
Driver={Arrow Flight SQL ODBC Driver};HOST=localhost;PORT=31338;useEncryption=true;disableCertificateVerification=true;UID=alice;PWD=demo-alice;RPCCallHeaders=tenant=acme;pool=bi
DuckDB (the CLI or the Python package, over DuckDB's native Quack protocol on port 9494, no driver; see DuckDB (native Quack)):
ATTACH 'quack:localhost:9494' AS qod (TYPE quack, TOKEN 'tenant=acme&pool=bi&user=alice&password=demo-alice');
SELECT c_name, c_phone, c_mktsegment FROM qod.tpch1.customer LIMIT 5;
Once connected, try:
SELECT c_name, c_phone, c_mktsegment FROM acme_tpch.tpch1.customer LIMIT 5;
As alice, c_phone comes back masked and only BUILDING-segment rows appear; reconnect as acme-admin and the same query returns full data.
Demo mode is insecure by design (self-signed TLS, open REST, demo credentials, ephemeral catalog). It is for evaluation only - never expose it or use it in production. For a durable install, use the paths below with your own Postgres.
Prerequisites
- Native path (Option A): a Postgres 16 or later reachable at
localhost:5432(the default) for the control-plane schema and tenant catalogs. Everything else (Java, DuckDB) is auto-provisioned byqod start. - Docker Compose path (Option B): just Docker. The stack ships its own Postgres, so you need nothing else installed.
- Ports 20900, 31338 and 9494 free on the host (admin REST/UI on 20900, FlightSQL edge on 31338, native Quack front door on 9494), for either path.
For alternative deployment paths (Kubernetes) and the full environment-variable model, see the Installation guide.
Boot the manager
Option A: Native (qod start)
If you have a reachable Postgres:
uv tool install qod # or: pip install qod
qod start
On first run qod start downloads the release jar from GitHub Releases (sha256-verified), self-installs the pinned DuckDB, creates the control-plane database (qod) when psql is available, then starts QoD. Stop it with qod stop (SIGTERM, then SIGKILL after 10 seconds).
On Windows (experimental), the same pip install qod && qod start works natively; see Run on Windows for the current gaps (no qod stop, no LOAD_* seeding).
Option B: Docker Compose (no prerequisites)
Only Docker is required - the stack ships its own Postgres. From the root of a cloned repository:
./scripts/docker/run-docker-compose.sh
This pulls the published starlakeai/quack-on-demand image plus a bundled postgres:16-alpine, brings the whole stack up, and waits for the manager to become ready - no local JDK or Postgres needed. Stop it with:
./scripts/docker/stop-docker-compose.sh
What comes up
Either path brings up the same surface:
- Admin REST + UI on
http://localhost:20900 - Arrow FlightSQL edge on
localhost:31338(TLS on, self-signed cert auto-generated undercerts/) - Native Quack front door on
quack:localhost:9494(plain HTTP by default; what a DuckDB clientATTACHes to) - Two admin accounts seeded -
adminand[email protected]- both with theADMIN_PASSWORDfrom.env(adminunless you change it before the first boot) - Two bootstrap tenants seeded from
src/main/resources/bootstrap-demo.yaml:acme(tenant-dbacme_tpch, poolsbi+etl) andglobex(tenant-dbglobex_tpcds, poolbi). Idempotent on restart.
Boot flags
The same flags work on both qod start and run-docker-compose.sh, and they combine:
| Flag | Effect |
|---|---|
LOAD_TPCH=N | Seeds TPC-H sf=N into acme/acme_tpch (8 tables in schema tpch1). The native path runs the loader on the host (DuckDB CLI + libduckdb are auto-installed by qod start on first boot, see Run from Linux/MacOS); the Compose path seeds inside the container. LOAD_TPCH=1 is ~6 M lineitem rows; SF=10 is ~60 M. Any seed flag being set also exports QOD_BOOTSTRAP_YAML so the manager imports the bundled demo manifest. |
LOAD_TPCDS=N | Seeds TPC-DS sf=N into globex/globex_tpcds (24 tables in schema tpcds1). Slower than TPC-H at the same SF (SF=10 ≈ several minutes; SF=100+ spills to disk). |
LOAD_SSB=N | Seeds the SSB (Star Schema Benchmark) star schema at sf=N: 5 tables (lineorder, customer, supplier, part, dwdate) derived from TPC-H dbgen into schema ssb1 of acme/acme_tpch, next to the TPC-H tables and served by the same acme pools. |
LOAD_TPC=N | Legacy shortcut: equivalent to setting LOAD_TPCH=N, LOAD_TPCDS=N, and LOAD_SSB=N. Explicit per-bench vars override it. |
DEMO=full|minimal | Bootstrap profile for the demo seed. full (the default) imports the two-tenant acme + globex manifest and exercises multi-tenancy, multiple pools, and federation; minimal imports bootstrap-demo-minimal.yaml instead - the shape for fronting a single DuckDB/DuckLake database: one tenant (acme), one pool (bi), one dual node serving both reads and writes, plus the analyst RLS/CLS demo. Only consulted when a LOAD_* flag is set and QOD_BOOTSTRAP_YAML is unset. DEMO=minimal with LOAD_TPCDS warns and skips the TPC-DS loader (no globex tenant in this profile). |
NUKE=1 | Tear down and wipe local state (Postgres data, parquet under ducklake/, certs/) before booting. Irreversible. |
QOD_VERSION=latest-snapshot | Use the latest snapshot image/jar instead of the latest release. |
| Build from source | Compose: BUILD=1 builds the image from the repo Dockerfile instead of pulling. Native: QOD_VERSION=BUILD runs sbt assembly first, and QOD_VERSION=LOCAL reuses the newest jar already in distrib/ without rebuilding. |
For a clean, freshly seeded environment in one shot, combine them. This wipes any previous state and boots with both demo datasets at scale-factor 1:
NUKE=1 LOAD_TPCH=1 LOAD_TPCDS=1 ./scripts/docker/run-docker-compose.sh
Any seed flag (or the legacy LOAD_TPC=1 shortcut, which enables all three) imports the bundled manifest under src/main/resources/bootstrap-demo.yaml, which declares the tenants, roles, groups, and users for both acme and globex; see the Access control model for the full ACL matrix.
To run the gateway in front of a single DuckDB instance instead, DEMO=minimal swaps in bootstrap-demo-minimal.yaml: one tenant (acme), one pool (bi), and one dual node serving both reads and writes, plus the analyst RLS/CLS demo. Bootstrap only imports into a fresh control plane, so switch profiles with NUKE=1:
NUKE=1 DEMO=minimal LOAD_TPCH=1 qod start
Pick one benchmark to keep boot snappy:
NUKE=1 LOAD_TPCH=1 ./scripts/docker/run-docker-compose.sh # TPC-H only, ~10 s seed
NUKE=1 LOAD_TPCDS=10 ./scripts/docker/run-docker-compose.sh # TPC-DS only, SF=10
The native path takes the same flags:
NUKE=1 LOAD_TPCH=1 LOAD_TPCDS=1 qod start
Open the admin console
Browse to http://localhost:20900 and log in with:
- Username:
admin - Password:
admin
You land on the Dashboard, the live cluster view with the recent-statements history. The tenant switcher at the top of the left sidebar lists the two bootstrap tenants, acme and globex; pick one and its Databases, Pools, and Auth Provider pages appear under Tenant in the sidebar (for example http://localhost:20900/ui/t/acme/pools). See the Admin UI guide.
Run your first query
DBeaver / JDBC
Install the Apache Arrow Flight SQL JDBC driver (org.apache.arrow:flight-sql-jdbc-driver, available on Maven Central). In DBeaver, create a new connection and paste this URL directly into the JDBC URL field:
jdbc:arrow-flight-sql://localhost:31338?useEncryption=true&disableCertificateVerification=true&user=admin&password=admin&tenant=acme&pool=bi&superuser=true
Set the driver class to org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver.
The disableCertificateVerification=true parameter is required because the gateway starts with an auto-generated self-signed certificate (see the TLS guide for how to supply a CA-signed cert and remove that flag).
The tenant=acme&pool=bi parameters are routing headers: the FlightSQL edge requires both to resolve which pool services the connection. Swap in tenant=globex&pool=bi to drive the TPC-DS demo instead.
The superuser=true parameter is required when login as admin/admin.
Python (ADBC)
Install the driver:
pip install --user adbc_driver_flightsql adbc_driver_manager
Run the bundled tpch-load-test as a one-shot client (defaults to the TPC-H workload against acme's tpch1 schema):
python3 ./scripts/bench/tpch-load-test/tpch-load-test.py \
--url grpc+tls://localhost:31338 --insecure \
--user admin --password admin --superuser \
--tenant acme --pool bi \
-w 1 -i 1 --warmup 0
--tenant and --pool are required (or set LT_TENANT / LT_POOL); the demo bootstrap creates tenants acme and globex, each with a pool named bi. The --insecure flag skips certificate verification for the auto-generated self-signed cert. The --superuser flag adds the superuser=true gRPC header so the bootstrap admin user (which lives in qodstate_user with tenant IS NULL) authenticates against the system realm; drop it when running as a tenant-scoped user.
Run the Python ADBC client from a neutral working directory (e.g. your home folder), not the repository root. Windows includes the current directory in the DLL search path, so a native arrow/duckdb DLL sitting in the checkout can get loaded into the client process ahead of the one shipped with adbc_driver_flightsql and crash it. The runner itself already forces UTF-8 output so the results table renders in a cp1252 console.
To switch workload, pass --workload tpcds (or set LT_WORKLOAD=tpcds). The runner ships two curated benchmarks:
| Workload | Default schema | Default tenant/pool wiring | Tables touched |
|---|---|---|---|
tpch (default) | tpch1 | acme / bi (demo bootstrap) | lineitem, customer, orders, nation, region, supplier, part |
tpcds | tpcds1 | globex / bi (demo bootstrap) | the 24 TPC-DS tables seeded by scripts/load-tpcds-dbgen.sh |
Each workload cycles a handful of representative queries (per-group aggregation, multi-way joins, top-N, window functions, date-range filters). The TPC-DS workload requires the target tenant-db to be seeded first:
SF=1 ./scripts/load-tpcds-dbgen.sh # seeds globex_tpcds.tpcds1
python3 ./scripts/bench/tpch-load-test/tpch-load-test.py --workload tpcds --tenant globex --pool bi \
--user admin --password admin --superuser --insecure -w 4 -i 50
Override the schema with --schema tpcds10 when you've seeded at a larger scale factor.
Example SQL
Once connected, run:
SELECT count(*) FROM tpch1.customer;
Tables live under the tpch1 schema inside the tpch tenant's database. The gateway auto-qualifies unqualified table names to the pool's default database and schema, so customer and tpch1.customer are equivalent once the session is scoped to the tpch/sales pool.
Next steps
- Installation - native-jar setup, Docker Compose, Kubernetes, and environment variable reference.
- Configuration reference - every
QOD_*/PROXY_*environment variable with its default and description. - REST API reference - the interactive API explorer for every
/api/*endpoint the admin UI calls.