SQLite Storage
Unified embedded SQLite store. Bundled, WAL-mode, refinery-managed migrations. Backup/restore tooling included.
Available since v0.8.0. Collapses every server-side persistent JSON
file (tokens.json, identity.json, rules.json) and the in-memory
audit ring buffer into one embedded SQLite database at
<data-dir>/state.db.
Why SQLite
- Single-binary deployment unchanged —
rusqlitewith thebundledfeature statically links a pinned SQLite. No system libsqlite3 dependency. - Crash-safe — WAL mode +
BEGIN IMMEDIATEwrite transactions. - Migrations — managed by
refinery; schema lives atcrates/portunus-server/src/store/migrations/.
Tables
Representative tables (schema evolves via migration; see
crates/portunus-server/src/store/migrations/):
| Table | Holds |
|---|---|
users | RBAC users (id, role, display name, disabled flag) |
credentials | Bearer-token hashes (blake3) + status |
grants | Per-user (client, listen-port range, protocols) triples |
rules | Pushed rules (with v0.11 nullable cap columns) |
rule_targets | Multi-target rule entries (v0.7+) |
audit | Durable audit log (replaces the v0.6 in-memory ring) |
rate_limit_owner | Per-owner cap envelopes (v0.11+) |
traffic_quotas, traffic_samples_* | Per-(user, client) monthly usage + history (v0.13+) |
web_sessions, login_attempts, onboarding_setup | Local password auth + first-run setup token (v1.1+) |
client_enrollments | One-time client enrollment codes |
Path layout
XDG-aligned default data-dir:
$XDG_STATE_HOME/portunus/ (or ~/.local/state/portunus/)
server.toml (optional — overrides built-in defaults)
server.crt
server.key
state.db
state.db-shm (present while server running)
state.db-wal (present iff WAL frames pending)Override with --data-dir. Production deployments typically use
/var/lib/portunus/.
Filesystem checks
The server refuses to start with event=startup.unsupported_filesystem
if --data-dir points at NFS, tmpfs, or ramfs. Move the data-dir to a
local writable filesystem.
It also refuses two portunus-server serve processes against the same
data-dir (event=startup.store_in_use, exit 75) — clustering is out of
scope.
Backup
Online backup via rusqlite::backup::Backup — WAL-aware, no quiescing
needed:
portunus-server backup --out ~/portunus-backup-$(date +%F).dbRefuses to overwrite an existing destination.
Restore
portunus-server reset --confirm # wipe local state first
portunus-server restore --in ~/portunus-backup-2026-05-08.db
portunus-server serve # schema migrations run automaticallyrestore validates the SQLite header magic and refuses to clobber a
non-empty data dir without --force. Backups whose schema is newer
than the binary's target version exit 78 with
startup.schema_version_too_new — restore on the matching binary
version, or upgrade.
Reset
portunus-server reset --confirm
# wipes state.db + state.db-wal + state.db-shm (+ state.db.lock)
# refuses any path whose first 16 bytes don't match the SQLite headerThe header check protects against a typo'd --data-dir. reset leaves
server.crt / server.key / server.toml in the same directory
untouched. Without --confirm it is a dry run that prints the path it
would remove.
Audit prune
The audit table is durable, not ring-buffered. Bound its size with the prune CLI:
# Dry run — count without deleting
portunus-server audit prune --before 2026-05-01T00:00:00Z --dry-run
# Real prune — deletes under BEGIN IMMEDIATE, then PRAGMA incremental_vacuum
portunus-server audit prune --before 2026-05-01T00:00:00ZRun it from cron (daily / weekly) to keep the database bounded.
Audit envelope mode
GET /v1/audit?since=<RFC3339>&until=<RFC3339>&cursor=<base64>&limit=N
returns { entries, next_cursor?, count } instead of the v0.7 array
root, enabling cursor-based historic scroll-back.
v0.7 callers (no since / until / cursor) keep getting the array
root unchanged — byte-stable.
portunus-client --bundle resolution
When --bundle is omitted the client searches in order:
$PORTUNUS_CLIENT_BUNDLE$XDG_CONFIG_HOME/portunus/client.bundle.json$HOME/.config/portunus/client.bundle.json./client.bundle.json
Exits 1 listing every attempted path when none resolve.