A thin, long-lived runtime for DuckDB's Quack protocol, packaged as a container image and a Helm chart.
DuckDB's quack extension turns any DuckDB session into a server (CALL quack_serve(...)), but ships no daemon: serving lasts only while the calling process holds its connection open. This project is that process — nothing more. It attaches a database (DuckLake first-class), starts quack_serve, and stays alive until SIGTERM. Authentication, authorization and the protocol itself belong to duckdb-quack and its ecosystem (e.g. quack-oauth); keeping this server simple is a design goal, not a limitation.
Serving a DuckLake this way means clients need only a URL and a token — PostgreSQL (catalog) and object storage (data files) stay private:
LOAD quack;
ATTACH 'quack:lake.example.com:443' AS lake (TOKEN '<token>');
FROM lake.query('USE lake'); -- once per session, see below
SELECT * FROM lake.my_table; -- reads, INSERT and CTAS work directly
SELECT * FROM lake.query('MERGE INTO ...'); -- UPDATE/DELETE/ALTER/MERGE/time travel- The server attaches your DuckLake (or runs an arbitrary
INIT_SQL_FILE) and callsquack_servewithallow_other_hostname = true. TLS termination is expected at the ingress/proxy layer, as recommended by the quack documentation. - Clients must switch to the attached database once per session (
FROM <alias>.query('USE lake')). The quack client sends SQL without the catalog qualifier, so unqualified names resolve in the server session's default database — which starts out as the empty in-memory one.USEmoves it; the server cannot do this on the client's behalf (search_pathcannot be set globally, and quack creates each session's connection without init SQL). Tables in schemas other thanmainneedUSE lake.<schema>, orSET search_path = 'lake.main,lake.<schema>'for several at once. - After that,
SELECT,INSERTandCREATE TABLE ... ASwork directly against<alias>.<table>.UPDATE,DELETE,ALTER TABLE,MERGE INTO, time travel (AT (VERSION => n)) and DuckLake table functions are not implemented in the quack client yet (independently of which database is used) — wrap those in<alias>.query('...'). - The client loads the catalog when
ATTACHruns, so tables created afterwards become visible on re-attach. If the server restarts, the session dies withInvalid connection idand the client has to re-attach. - One replica = one writer. DuckLake's optimistic concurrency handles concurrent commits from other writers, but this server is intentionally a single process.
All configuration is via environment variables. In Kubernetes they are injected from an existing Secret (envFrom); see the Helm section below.
| Variable | Required | Default | Description |
|---|---|---|---|
QUACK_TOKEN |
yes | Client authentication token | |
QUACK_PORT |
9494 |
Listen port | |
CATALOG_HOST |
yes* | DuckLake catalog (PostgreSQL) host | |
CATALOG_PORT |
5432 |
Catalog port | |
CATALOG_DB |
yes* | Catalog database name | |
CATALOG_USER |
yes* | Catalog user | |
CATALOG_PASSWORD |
yes* | Catalog password | |
S3_ENDPOINT |
yes* | Object storage endpoint | |
S3_ACCESS_KEY |
yes* | Object storage access key | |
S3_SECRET_KEY |
yes* | Object storage secret key | |
DATA_PATH |
yes* | Data path, e.g. s3://ducklake/main |
|
S3_USE_SSL |
false |
Use TLS for object storage | |
S3_REGION |
us-east-1 |
Object storage region | |
ATTACH_ALIAS |
lake |
Alias of the attached database | |
INIT_SQL_FILE |
SQL file to run instead of CATALOG_* |
* required unless INIT_SQL_FILE is set.
The chart references an existing Secret for the token and connection settings (auth.existingSecret, injected via envFrom). Create it with your own tooling — plain kubectl create secret, External Secrets Operator, etc.
kubectl create secret generic quack-server -n quack \
--from-literal=QUACK_TOKEN=... \
--from-literal=CATALOG_HOST=postgres-cluster-rw.postgres \
--from-literal=CATALOG_PORT=5432 \
--from-literal=CATALOG_DB=ducklake_catalog \
--from-literal=CATALOG_USER=ducklake \
--from-literal=CATALOG_PASSWORD=... \
--from-literal=S3_ENDPOINT=http://rustfs.rustfs:9000 \
--from-literal=S3_ACCESS_KEY=... \
--from-literal=S3_SECRET_KEY=... \
--from-literal=DATA_PATH=s3://ducklake/main
helm install quack charts/simple-quack-server -n quack \
--set auth.existingSecret=quack-serverExpose it with the standard ingress values (ingress.enabled, ingress.hosts). Remote clients must specify the ingress port explicitly (quack:host:443) because the quack default port 9494 is usually not proxied.
Tools are managed with mise: mise install.
just serve # run locally (configuration via environment variables)
just test # lifecycle test: local DuckLake, client roundtrip, SIGTERM
just image-build # build the image
just helm-lint # lint the chart
just helm-template # render the chart
just client-test localhost:9494 <token> # smoke test a running server
just helm-test <namespace> # run the chart's test hook (read-only)just test needs nothing but uv and the ducklake extension: it starts the
server on a temporary local DuckLake, attaches as a client, checks the USE
contract with a read and an insert, then requires SIGTERM to stop the server
cleanly. just helm-test runs helm test, which attaches to a deployed release
from inside the cluster and reads the catalog — no writes to the real lake. Set
tests.enabled=false to leave the hook out of the release.
- Clients assume HTTPS for non-localhost hosts. Inside a cluster (plain HTTP), attach with
(DISABLE_SSL true); behind a TLS-terminating ingress, omit it. - Earlier versions mirrored the attached tables as views in the default database so that
remote.<table>resolved withoutUSE. That has been removed: the mirror views shadowed the real tables in the client's catalog (making them read-only even afterUSE), covered themainschema only, and lagged behind DDL. A client that cannot issue a single statement at session start cannotATTACHeither, so nothing is lost. - Authentication is a single shared token for now. For OAuth 2.1 / OIDC (per-user tokens, claim-based authorization, audit), see quack-oauth — planned as an optional integration.