libSQL / sqld · self-hosted · one operator per box

A private SQLite platform on your own 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.

sqlitend · create → connect → done, end to end
$ 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"; …' 
bind 0.0.0.0 · per-DB keys · tokens · REST API · metrics 1-vCPU VM · Bun 1.4.2 · sqld 0.24.32
RuntimeBun ≥ 1.1 · Hono · React
Enginesqld 0.24.32 pinned
Default bind0.0.0.0 · all interfaces
Deployone-line · no root

What it is

SQLite scale-out isn't the point.
Being able to self-host 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.

Isolation by key, not by folder

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.

Full-access tokens with a lifespan

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).

Subprocesses, not containers

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.

Live per-database metrics

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

Measured, not marketed.

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.

Stress test
2,240,800 ops · 0 failures

The One-Gigabyte Test

  • 2 workspaces × 10 databases = 20 DBs
  • 200,000 rows seeded at ~10k rows/s per DB
  • 8-round mixed CRUD hammer; COUNT(*) exact on every DB
  • Destroy phase clean — no orphaned dirs, no leaks
Full stress report raw evidence ↗
Benchmark
17.4× network tax · 3.4× batched

The Speedtest Replication

  • Official SQLite speedtest1 (3.53.4): TOTAL 6.494s on this box
  • Statement-level port: embedded 483ms vs sqlitend/sqld 8,385ms
  • The honest per-statement network tax — 1,000-row batches collapse it to 3.4×
Full benchmark report raw evidence ↗
Concurrency
1,000,000 statements · 0 failures

The Million-Statement Run

  • Read-heavy 80/20, 4 clients, over loopback Hrana
  • 1,500/s mean · p50 2ms / p95 6ms / p99 11ms
  • Host peaked at 357MB RAM · sqld RSS only 42MB
  • 26 BUSY retries (0.0026%) — the client retried, the DB never dropped one
Full concurrency report raw evidence ↗
82 tests passing · bun test 5 / 5 reviewer PASS · security · reliability · frontend · tests · ops 0 type errors · tsc --noEmit, server + web ≈ 4s e2e round trip · create → connect → delete 0 regressions · prod-readiness gate 2026-09-06

The single-writer wall

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

One command. Under a minute. No root.

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 ↗).

install 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.

01

Install

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
02

Start

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
03

Create + token

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}
04

Connect

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 default
SQLITEND_PUBLIC_HOSThost advertised in connection URLs
SQLITEND_PORT_RANGEper-DB http+grpc pair · 6101-6300
SQLITEND_DATA_ROOTdata root · ~/.local/share/sqlitend
SQLITEND_TOKEN_TTL_HOURSdefault token lifetime · 24h
SQLITEND_SAMPLE_INTERVAL_MSmetrics sample + poll interval

Explore the API

The whole control plane is one REST surface.

JSON in, JSON out. Every mutating call is validated with zod — strict 400s, no silent defaults. This table is the complete route surface.

MethodPathWhat it does
GET/api/systemVersion, sqld status, publicHost, workspace & database counts
GET/api/workspacesList workspaces
POST/api/workspacesCreate a workspace — {"name"}
DELETE/api/workspaces/:idDelete a workspace — must be empty (409 otherwise); delete its databases first
GET/api/databases · /api/databases/:idList / inspect databases — status, port pair (connection URLs via /connection)
POST/api/workspaces/:id/databasesCreate a database — spawns its supervised sqld
POST/api/databases/:id/start · /stopStart / stop the sqld process
DELETE/api/databases/:idKill + remove the database and its data directory
GET/api/databases/:id/connectionLive connection URL + current token for any client
GET/api/databases/:id/tokensList issued tokens
POST/api/databases/:id/tokensMint a full-access token — {"expiresInHours"}
DELETE/api/databases/:id/tokens/:jtiRevoke by jti — 501 in v1 (stateless JWT); use short TTLs or re-create the DB
GET/api/databases/:id/metricsSampled CPU / RSS / disk / uptime (5s default)

Web dashboard: same data at :6100, served by the same backend through the API proxy.