Skip to main content
Version: 0.9.11

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?​

CommandWhat it isNeeds
qod serve --demothrowaway showcase on sample data, insecure by designnothing
qod serve ./your-datapersistent gateway over your own data, secure defaultsnothing
qod startyour deployment: your own Postgres, your configPostgres + qod setup

Demo admin console​

Browse to http://localhost:20900/ui/ and log in with:

Tenant IDUsernamePasswordView
leave blankrootdemo-rootsuperuser console (all tenants)
leave blankadminadmin, or your QOD_ADMIN_PASSWORD if exportedsuperuser console (all tenants)
leave blank[email protected]same as adminsuperuser console (all tenants)
acmeacme-admindemo-acme-adminacme-scoped roles, groups, users, pools
acmealicedemo-aliceacme-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:

UsernamePasswordAccess
alicedemo-aliceanalyst: customer, orders, nation, region read-only; c_phone masked to ***; only c_mktsegment = 'BUILDING' rows; any other table denied
acme-admindemo-acme-admineverything in acme, full and unmasked
rootdemo-rootsuperuser, bypasses per-statement ACL
adminadminsuperuser, 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.

warning

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 by qod 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 under certs/)
  • Native Quack front door on quack:localhost:9494 (plain HTTP by default; what a DuckDB client ATTACHes to)
  • Two admin accounts seeded - admin and [email protected] - both with the ADMIN_PASSWORD from .env (admin unless you change it before the first boot)
  • Two bootstrap tenants seeded from src/main/resources/bootstrap-demo.yaml: acme (tenant-db acme_tpch, pools bi + etl) and globex (tenant-db globex_tpcds, pool bi). Idempotent on restart.

Boot flags​

The same flags work on both qod start and run-docker-compose.sh, and they combine:

FlagEffect
LOAD_TPCH=NSeeds 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=NSeeds 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=NSeeds 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=NLegacy shortcut: equivalent to setting LOAD_TPCH=N, LOAD_TPCDS=N, and LOAD_SSB=N. Explicit per-bench vars override it.
DEMO=full|minimalBootstrap 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=1Tear down and wipe local state (Postgres data, parquet under ducklake/, certs/) before booting. Irreversible.
QOD_VERSION=latest-snapshotUse the latest snapshot image/jar instead of the latest release.
Build from sourceCompose: 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.

Windows

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:

WorkloadDefault schemaDefault tenant/pool wiringTables touched
tpch (default)tpch1acme / bi (demo bootstrap)lineitem, customer, orders, nation, region, supplier, part
tpcdstpcds1globex / 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.