libSQL / sqld · self-hosted · one operator per box
sqlitend runs sqld (libSQL's embeddable server) as plain subprocesses on the machine you already have — no containers, no cluster, no vendor. Create a database, get a connection URL and a token, and point any libSQL client at it. The whole round trip takes under a minute.
$ curl -fsSL https://raw.githubusercontent.com/thaqiif/sqlitend/main/scripts/install.sh | bash ✔ Bun · repo · pinned sqld (SHA-256) · web UI built ≈ 1 min $ ~/.local/bin/sqlitend ✔ control plane on http://0.0.0.0:6100 — public by default $ curl -s -X POST http://localhost:6100/api/workspaces -d '{"name":"labs"}' ✔ workspace labs · id 8ec2… $ curl -s -X POST http://localhost:6100/api/workspaces/8ec2…/databases -d '{"name":"users"}' ✔ database users → sqld on :6121 · ws://<public-host>:6121 $ curl -s -X POST http://localhost:6100/api/databases/…/tokens -d '{"expiresInHours":24}' ✔ token issued · full access · 24h TTL $ bun -e 'import { createClient } from "@libsql/client"; …'
What it is
SQLite is the most deployed database in the world, and sqld turns each one into a real network server: Hrana — libSQL's WebSocket wire protocol — over ws://, HTTP, live replicas. The piece everyone was missing is the boring operator — something to create, run, secure, monitor, and retire databases without standing up yet-another-control-plane. sqlitend is that operator: one per machine, REST on the front, plain directories on disk.
Point a libSQL client at a URL and a database answers — exactly like a hosted service, except the data never leaves your box and nothing in the stack is bigger than one binary and one Bun process.
Every database gets its own sqld process on the pinned, SHA-256-verified binary. The control plane supervises it: launch, ready-probe, crash detection, restart adoption, clean delete. A database is a first-class object with an id, a data directory, a per-DB http+grpc port pair (6101–6300), and metrics — not a container you have to feed.
Before this page existed, sqlitend was load-tested and reviewer-gated. The numbers are below, with the raw evidence JSON linked on each card.
Each database owns an Ed25519 signing keypair. Tokens are signed per-database — a token for users is rejected by orders. Cross-DB access is cryptographically impossible, not just policy.
Issue full-scope tokens that expire (24h default). No shared platform password. sqld validates JWTs statelessly, so revoke-by-jti is not supported in v1 — to drop a leaked token, use a short TTL or delete and re-create the database (it re-keys).
No Docker daemon, no orchestration. The pinned binary runs supervised under the control plane — crash recovery and restart adoption included. The whole platform is a couple of processes.
Per-DB CPU and RSS sampled straight from /proc, served to the dashboard and to /api/databases/:id/metrics. See a hot database before your users do.
The measurements
Every number below was run against the real product on a throwaway 1-vCPU / 969MB / aarch64 VM (Debian 13) on 2026-09-06, and cross-checked against the raw evidence JSON linked on each card. No synthetic cloud box, no pre-warmed benchmark cluster — the smallest machine this platform will ever be run on.
sqld is single-writer. Aggregate write throughput is flat at ~2,250/s from 1 to 4 clients on one core, declines past 8, and thrashes beyond 16 — that is sqld's design, not a bug in sqlitend. The lever is batching: single-row insert overhead drops from ~20× to 3.4× at 1,000 rows per statement (point lookups stay ~100× — per-statement by nature).
One finding worth knowing before write-heavy HA: a sustained write-heavy run left the benchmark sqld process busy-spinning at 100% CPU with zero clients attached (host load ~56 on one core). It killed cleanly, and the read-heavy million never showed it — but batch writes, pool connections, and watch per-DB sqld CPU. The read side is a different story: 80/20 at a million statements, zero errors.
And in v1 the control plane itself has no login — on a public bind, put it behind a firewall or an authenticated reverse proxy.
Run it
The installer handles everything: Bun if missing, the repo, the pinned sqld binary (SHA-256 verified), the web build, and a sqlitend launcher in ~/.local/bin. All data and keys live under ~/.local/share/sqlitend. Upgrading later: re-run the same installer against a newer SQLITEND_BRANCH; when the pinned sqld version moves, the new checksum ships in fetch-sqld.sh (upgrade SOP ↗).
curl -fsSL https://raw.githubusercontent.com/thaqiif/sqlitend/main/scripts/install.sh | bash
Before you pipe it: read install.sh and the per-OS sqld checksums pinned in fetch-sqld.sh — the installer verifies them on every run.
One line, about a minute. Options: SQLITEND_BRANCH, SQLITEND_INSTALL_DIR, SQLITEND_SYSTEMD=1 for a systemd user service.
# terminal
curl -fsSL …/scripts/install.sh | bash
Control plane binds 0.0.0.0:6100 by default — reach it from any interface on the LAN/VPN. SQLITEND_HOST=127.0.0.1 goes local-only.
# terminal
~/.local/bin/sqlitend
Workspace → database → token via REST (or the dashboard at :6100). The API table below is the whole surface.
POST /api/workspaces {"name":"labs"}
POST /api/workspaces/:id/databases {"name":"users"}
POST /api/databases/:id/tokens {"expiresInHours":24}
Any libSQL client. Set SQLITEND_PUBLIC_HOST so advertised URLs point at your machine, not loopback.
// any libSQL client
const c = createClient({
url: "libsql://<host>:6121",
authToken: token,
});
await c.execute("select * from users");
SQLITEND_HOSTbind address — 0.0.0.0 defaultSQLITEND_PUBLIC_HOSThost advertised in connection URLsSQLITEND_PORT_RANGEper-DB http+grpc pair · 6101-6300SQLITEND_DATA_ROOTdata root · ~/.local/share/sqlitendSQLITEND_TOKEN_TTL_HOURSdefault token lifetime · 24hSQLITEND_SAMPLE_INTERVAL_MSmetrics sample + poll intervalExplore the API
JSON in, JSON out. Every mutating call is validated with zod — strict 400s, no silent defaults. This table is the complete route surface.
| Method | Path | What it does |
|---|---|---|
| GET | /api/system | Version, sqld status, publicHost, workspace & database counts |
| GET | /api/workspaces | List workspaces |
| POST | /api/workspaces | Create a workspace — {"name"} |
| DELETE | /api/workspaces/:id | Delete a workspace — must be empty (409 otherwise); delete its databases first |
| GET | /api/databases · /api/databases/:id | List / inspect databases — status, port pair (connection URLs via /connection) |
| POST | /api/workspaces/:id/databases | Create a database — spawns its supervised sqld |
| POST | /api/databases/:id/start · /stop | Start / stop the sqld process |
| DELETE | /api/databases/:id | Kill + remove the database and its data directory |
| GET | /api/databases/:id/connection | Live connection URL + current token for any client |
| GET | /api/databases/:id/tokens | List issued tokens |
| POST | /api/databases/:id/tokens | Mint a full-access token — {"expiresInHours"} |
| DELETE | /api/databases/:id/tokens/:jti | Revoke by jti — 501 in v1 (stateless JWT); use short TTLs or re-create the DB |
| GET | /api/databases/:id/metrics | Sampled CPU / RSS / disk / uptime (5s default) |
Web dashboard: same data at :6100, served by the same backend through the API proxy.