Host a SQL file
n6kd is a single Go binary. Point it at a .sql file and it becomes a server
that DuckDB clients can attach to:
n6kd -f schema.sql
That is the entire application. There is no framework, no ORM, no endpoint to write, and nothing in the path that has to be kept in sync with the schema — the schema is the service.
What it prints
INFO lockdown on: served connections cannot reach the filesystem
INFO ran schema.sql (1551 bytes)
INFO boot SQL finished in 44ms
INFO startup finished in 55ms
INFO ready — clients attach with:
INFO ATTACH 'n6k://localhost:8080' AS demo (TYPE n6k)
The last lines are the point: it tells you the statement your users need. Paste it into any DuckDB with the extension loaded and the tables are there.
The schema file
Ordinary DuckDB SQL. Whatever it creates is what gets served:
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR,
region VARCHAR,
signed_up DATE
);
INSERT INTO customers
SELECT i, 'Customer ' || i,
['NA', 'EMEA', 'APAC', 'LATAM'][1 + (i % 4)],
DATE '2024-01-01' + (i % 365)
FROM range(1, 5001) t(i);
It is a DuckDB script, so it can also read Parquet or CSV, attach Postgres, or build derived tables — anything the loader can run at boot, the server can then serve.
Flags
| Flag | Default | Effect |
|---|---|---|
-f | — | SQL file run at boot; repeatable, in the order given with -c |
-c | — | SQL statements run at boot; repeatable, in the order given with -f |
--perms | — | perms.yml naming the catalogs to serve bridged |
--addr | :8080, or $PORT | Listen address |
--api-key | $N6KD_API_KEY | Require this bearer token; without it, anyone who reaches the port is served |
--lockdown | on | Served connections cannot reach the filesystem |
--no-lockdown | — | The inverse, for when client SQL is trusted |
--debug | $N6KD_DEBUG | Per-connection and per-catalog logging |
--json-logging | $N6KD_JSON_LOGGING | JSON log lines with Cloud Logging severity fields, for log collectors |
Two subcommands matter operationally: n6kd install downloads the DuckDB
extensions into the extension directory, and n6kd selftest loads them and
bridges a throwaway catalog end to end. selftest is worth running as the last
step of a container build — a green build then means the extensions actually
load on that image, rather than failing on first connect in production.
Lockdown
By default, SQL arriving from a client cannot touch the host filesystem:
CALL demo.exec('SELECT count(*) FROM read_csv(''/etc/passwd'')');
Permission Error: n6k[demo] EXEC: Cannot access file "/etc/passwd"
- file system operations are disabled by configuration
This matters because you are handing out a SQL interface. DuckDB is a capable
engine — read_csv, COPY … TO, INSTALL, and ATTACH all reach outside the
database by design. Lockdown removes that reach from served connections while
leaving it available to the schema file at boot, which is a different trust
level.
A container
The image is the binary plus your data file:
FROM srossross/n6kd:latest
COPY schema.sql /app/schema.sql
docker run -p 8080:8080 -e N6KD_API_KEY=secret your-image
It is stateless and reads $PORT, so any container host takes it unchanged.
The demo server behind these pages is this image on Cloud Run.
A schema built into memory at boot is rebuilt on every restart, and writes are lost with the process. That is the right shape for a demo or a read-mostly published dataset, and the wrong shape for a system of record. Point the schema at durable storage when you need writes to survive.
What it does not do
n6kd serves whatever the schema file created, to anyone holding the token,
with full read and write access. That is fine for a dataset you are publishing
and wrong for anything else — which is what the optional second file is for.