Getting started
quicSQL is one static binary. This page takes you from nothing to a running server your language can query, in about a minute.
Install
docker run -p 7775:7775 -v quicsql-data:/data \
-v ./quicsql.yaml:/etc/quicsql/quicsql.yaml \
ghcr.io/quicsql/quicsql:latest1. Run the daemon
One YAML file describes the whole server: listeners (one per transport), databases (one per line), and — when you want them — secrets, TLS, principals, and grants.
# quicsql.yaml
server:
data_dir: ./data
secrets:
- {name: keys, type: file, dir: ./data/keys} # "keys:<name>" reads ./data/keys/<name>
tls:
dev: {mode: self_signed, hosts: [localhost, 127.0.0.1]} # use mode: files in production
listeners:
- {name: h1, transport: h1, address: 127.0.0.1:7775}
- {name: h2, transport: h2, address: 127.0.0.1:7777, tls: dev}
- {name: h3, transport: h3, address: 127.0.0.1:7777, tls: dev, advertise: true} # HTTP/3 over QUIC — shares the h2 port (UDP vs TCP)
- {name: unix, transport: unix, address: ./data/quicsql.sock, socket_mode: "0600"}
databases:
- {name: users, backend: file, path: users.db, mode: rwc, pragmas_preset: recommended}
- name: orders # encrypted + compressed at rest
backend: vault
path: orders.vault
vault: {compression: default, cipher: adiantum, key: keys:orders}quicsql --config quicsql.yamlWarning
With no principals or grants configured, the server runs in open mode —
every caller is read-write. That’s the right default for a loopback dev
server; bind to 127.0.0.1 and nothing else. To lock it down, add a
principal, a grant, and a listener auth: list — see
Auth & authorization.
Every listener serves the same endpoints: POST /<db>/query (native JSON),
/<db>/v2|v3/pipeline and /<db>/v3/cursor (Hrana), /<db>/export,
/<db>/backup, /<db>/changeset/*, /<db>/blob/*, /<db>/changes (the SSE
change feed), plus the server-scoped /_health, /_metrics, /_admin/*,
/_auth/challenge, /_auth/session, and /_auth/enroll. The canonical port is
7775 (h1); the sequence continues h2c 7776 and h2 7777 — and h3 shares
7777 (QUIC/UDP alongside h2’s TLS/TCP, the way HTTPS shares :443; the h3
listener’s advertise: true emits Alt-Svc so clients auto-upgrade).
Routing: addressing a database by path or host
Every request names one database. How the server derives that name from the
request is an optional top-level routing: block:
routing:
by_path: true # DB is the first path segment: /<db>/query (the default)
by_host: false # DB is a Host subdomain (needs host_suffix)
host_suffix: .db.example.com # users.db.example.com → database "users"
default_db: "" # fall back to this DB when neither path nor host names oneOmit routing: entirely and path routing is on — by_path defaults to
true when both by_path and by_host are unset, which is what the
snippets above rely on (/users/query). Turn on by_host with a host_suffix
to address databases by subdomain instead; set both and a database named in the
path wins over the Host. default_db supplies the database when a request
carries neither.
TLS profiles
A tls: profile supplies a listener’s certificate, via one of three modes:
files—cert:/key:PEM paths. The production choice (your own cert).self_signed— the dev generator above: an in-memory cert for the listedhosts. Clients see an untrusted-cert warning until they add an exception.qip— auto-fetch a browser-trusted qip.sh wildcard cert for a private network or localhost, with no CA setup.subdomainpicks the qip.sh zone (defaulti.qip.sh, whose*.i.qip.shnames resolve to 127.0.0.1);refreshsets the cert-reload interval (default 12h). It needs outbound network access at startup to fetch the cert.Security caveat. qip.sh publishes the certificate’s private key publicly — that’s how it hands out a trusted cert for a name anyone can point at their own loopback. So a
qipcert gives you encryption and a valid padlock, but not server authentication: a man-in-the-middle on the same private network can serve the same cert and impersonate the server. Use it for localhost and trusted LANs; usefiles(your own cert) for anything untrusted parties can reach. The server logs a warning if aqiplistener binds a non-loopback address.
2. Talk to it — from your language
The native JSON endpoint takes {sql, args} — or a statements batch, which
runs as one explicit transaction, all-or-nothing:
curl -s http://127.0.0.1:7775/users/query \
-d '{"sql":"CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, name TEXT)"}'
curl -s http://127.0.0.1:7775/users/query \
-d '{"statements":[
{"sql":"INSERT INTO users(name) VALUES (?)","args":["ada"]},
{"sql":"SELECT * FROM users"}
]}'Integers stay exact on the wire; blobs are boxed as {"base64": …}. Full
shapes in the HTTP API reference.
These are dev-mode snippets (open mode, no token). Once you add principals,
every SDK passes its token via authToken / auth_token, which quicSQL
receives as standard bearer auth — see Clients & languages.
3. Embed it (Go)
serverd.Run assembles the whole pipeline in-process — for tests, custom SQL
functions, or shipping a bundled server inside your own binary:
import "quicsql.net/serverd"
inst, _ := serverd.Run(cfg, log) // cfg is a *config.Config; returns an *Instance
defer inst.Shutdown(ctx)Where next
- Clients & languages — your language’s path in, with CI-tested examples: JavaScript/TypeScript, Python, PHP, Go, and more.
- Databases & backends — every open mode gosqlite has, over the wire: files, in-memory, mvcc snapshots, and vault containers in every shape, plus pragmas, pool tuning, and secrets.
- Auth & authorization — seven authentication methods
(including short-lived session tokens and device enrollment for browser apps),
the
none < read-only < read-write < admincapability model, and why read-only cannot be talked around. - The Hrana pipeline — transactions, batches, batons, and production limits.
- The change feed —
GET /<db>/changesstreams committed row changes over Server-Sent Events (resume, filter, reset). - Administration — the
/_admincontrol plane, online backup and in-place restore, WAL checkpoint, and vault maintenance. - Runnable examples — every language above, asserting its results against a real server in CI.