SQLite, served.
quicSQL turns a SQLite database — a plain file, in-memory, or an encrypted vault — into an authenticated network service any language can query: a dead-simple JSON API, and the libSQL protocol your existing SDKs already speak.
Your language already has a client
quicSQL serves the libSQL Hrana protocol natively — the official SDKs for TypeScript, Python, PHP, Ruby, Rust, Swift, and Elixir connect by URL alone, interactive transactions included. Verified in CI against the real SDKs.
curl is a client
The native JSON API is one endpoint: POST /db/query with {sql, args} or an all-or-nothing statements batch. If it can send an HTTP request, it can query quicSQL — no SDK required.
Serves what nothing else can
An encrypted + compressed vfs/vault container is only safe under a single owner. quicSQL is that owner: it opens the vault once and multiplexes every client through it — encryption at rest, shared over the network.
Every transport, one handler
HTTP/1.1, cleartext h2c, HTTP/2 over TLS, HTTP/3 over QUIC, and Unix domain sockets — the identical handler on every wire, so semantics never change with the transport.
Batteries included
Six authentication methods — bearer, HTTP-basic, mTLS, ed25519 challenge/response, Unix peer credentials, or none — per-database capability grants, a /_admin control plane, audit log, OpenMetrics, rate limits, and query timeouts.
Read-only means read-only
A read-only principal runs on a connection in PRAGMA query_only plus a write-denying authorizer — enforced by the engine, not by parsing SQL. You cannot talk your way past it.
One static binary
Pure Go, CGo-free, built on gosqlite. Cross-compiles with plain GOOS=… go build, ships as a distroless container or a single file you scp anywhere. No C toolchain, no runtime dependencies, ever.
ORMs ride along, unchanged
Drizzle and Prisma (TypeScript), SQLAlchemy (Python), Ecto (Elixir), and LiteORM (Go) all run over the wire as-is — their libSQL drivers already speak quicSQL’s protocol.
First-class Go, embeddable
A native client for every transport and auth method, a database/sql driver where only the DSN changes, and serverd.Run(cfg, log) to embed the whole server in-process.
Up and running in a minute
One YAML file: listeners per transport, databases per line. With no principals configured the server runs in open mode — bind to loopback, then add auth when you need it.
# quicsql.yaml
server:
data_dir: ./data
secrets:
- {name: keys, type: file, dir: ./data/keys}
tls:
dev: {mode: self_signed, hosts: [localhost, 127.0.0.1]}
listeners:
- {name: h1, transport: h1, address: 127.0.0.1:7775}
- {name: h3, transport: h3, address: 127.0.0.1:7778, tls: dev}
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}Run it, and the native JSON endpoint is thin enough for curl:
$ quicsql --config quicsql.yaml
$ curl -s http://127.0.0.1:7775/users/query \
-d '{"sql":"SELECT name FROM users WHERE id = ?","args":[7]}'The same handler is listening on HTTP/3 — and on h2c, HTTP/2, and Unix
sockets if you add those listeners. Endpoints per database:
/query, /v2|v3/pipeline, /v3/cursor, /export, /changeset/*,
/blob/*; server-wide: /_health, /_metrics, /_admin/*.
Talk to it from anything
The official libSQL SDK connects by URL alone — transactions, batches, and all. (Keep the trailing slash: quicSQL namespaces databases by path.)
import { createClient } from "@libsql/client";
const db = createClient({
url: "http://127.0.0.1:7775/users/", // trailing slash!
authToken: "your-token",
});
const rs = await db.execute("SELECT name FROM users WHERE id = ?", [7]);
const tx = await db.transaction("write");
await tx.execute({ sql: "UPDATE accounts SET balance = balance - ? WHERE id = ?", args: [100, 1] });
await tx.execute({ sql: "UPDATE accounts SET balance = balance + ? WHERE id = ?", args: [100, 2] });
await tx.commit();How it compares
| Capability | quicSQL | libSQL sqld | rqlite | ws4sql |
|---|---|---|---|---|
| Works with the existing libSQL SDKs (TS, Python, PHP, Ruby, Rust, …) | ✓ | ✓ | ✗ | ✗ |
| Simple JSON-over-HTTP API | ✓ | ✓ | ✓ | ✓ |
| Pure Go, CGo-free, one static binary | ✓ | ✗ (Rust) | ✗ (CGo driver) | ✗ (CGo driver) |
| HTTP/3 (QUIC) transport | ✓ | ✗ | ✗ | ✗ |
| Unix socket + peer-credential auth | ✓ | ✗ | ✗ | ✗ |
| Built-in auth (mTLS, bearer, ed25519, password) | ✓ | partial | ✓ | partial |
| Per-database authz, read-only enforced in-engine | ✓ | ✗ | ✗ | partial |
| Encrypted + compressed database, served live | ✓ (vfs/vault) | encryption only | ✗ | ✗ |
| Runtime control plane + audit log | ✓ | partial | ✗ | ✗ |
| Distributed replication / Raft consensus | ✗ | ✓ (Turso) | ✓ | ✗ |
The trade-off is deliberate. quicSQL is a single-owner multiplexer, not a replicated cluster. If you need Raft consensus and multi-node failover, rqlite and Turso are built for that. quicSQL is built to make one powerful SQLite database — especially an encrypted vault — safely and richly network-accessible, from every language you run.
Your ORM already works here
Because quicSQL speaks the protocols your data layers already use, declarative stacks run over the wire unchanged:
- Drizzle and Prisma in TypeScript
- SQLAlchemy in Python
- Ecto in Elixir
- LiteORM in Go — whose typed vector, full-text, and hybrid search execute server-side against the same database
import (
"liteorm.org/dialect/sqlite"
"liteorm.org/orm"
_ "quicsql.net/client/sqldriver" // registers quicsql://
)
// A local file in dev, a quicSQL server in prod —
// only the DSN changes:
db, _ := sqlite.Open("quicsql://127.0.0.1:7777/app?transport=h2&token=…")
defer db.Close()
orm.AutoMigrate[User](ctx, db) // runs the DDL on the server