TrailBase
SQLite-as-API — the dashboard's application data store for everything that isn't a YAML config.
TrailBase is the dashboard's application database. It exposes SQLite tables over an HTTP API (with row-level auth, schema validation, and a typed TS SDK). The dashboard treats it as a managed Postgres-replacement for small structured data — feed metadata, panels, groups, layouts, audit lines.
Anything geographic / political with a long lifecycle that's edited in the GUI lives here. Anything that's a static config or registry lives in a YAML file in the repo.
Where it sits
| In-cluster URL | (set via TRAILBASE_URL env on the dashboard) |
| Chart / values | r2d2-fleet/k8s/trailbase/ in the parent repo |
| Schema source of truth | r2d2-fleet/k8s/trailbase/configmap-schema.yaml |
| Persistent storage | Longhorn PVC |
| Auth | Session cookies issued by TrailBase, set by the login flow |
Tables the dashboard owns
| Table family | What it stores | Read by |
|---|---|---|
restreamer_feed_meta | alias, platform_id, tags, notes per feed | HoloVids, Holoprojector |
restreamer_layouts | cols × rows + tile rectangles | Holoprojector LayoutEditor |
restreamer_panels | layout ref + per-slot bindings + name + public token | Holoprojector |
restreamer_groups | ordered list of panel IDs + name + public token | Holoprojector groups |
restreamer_platforms | platform records (e.g. "M300 #14") | HoloVids feed detail |
restreamer_tags | tag strings | HoloVids filters |
republic_* | nations, ShopKeepers, holdings | Republic |
archives_* | per-archive records | Temple Archives |
holocron_* | site config overrides (when present) | Galaxy Map |
Session cookies
When AUTH_ENABLED=true, mutating routes require a TrailBase session cookie. The
login flow exchanges a Keycloak token for a TrailBase session and sets the cookie on
the dashboard's domain. Read routes stay public-within-cluster; only mutations gate.
Magic URLs (/p/<token>) bypass auth by design — see Restreaming.
Schema migrations
The schema is declarative — it lives in configmap-schema.yaml, and TrailBase
applies it on container start. To add a column:
- Edit the schema ConfigMap.
- Roll the TrailBase Deployment (or restart the pod).
- Update the dashboard's TS shape (manually today; auto-generated from the schema is a future improvement).
For larger reshapes (renaming a table, splitting a column), the migration is done out- of-band — drain dashboard writes, snapshot, apply, restore.
Records API + the PK constraint
TrailBase v0.27's Records API exposes a SQLite table over HTTP at
/api/records/v1/<name> — but only if the table's primary key is INTEGER or
BLOB with an is_uuid_v7 CHECK constraint. A table with a TEXT PRIMARY KEY
(e.g. feed_id, instance_id, cache_key) is rejected at config-load time with
Does not have a suitable PRIMARY KEY column.
Each table the dashboard wants to read/write through the Records API needs a
record_apis { name: "<table>" table_name: "<table>" acl_authenticated: [...] }
block in configmap-config.yaml. No block → every call returns 405 Method Not Allowed.
Today only four tables qualify: audit_log, fleet_snapshot, ship_revisions,
aircraft_revisions — they all use INTEGER PRIMARY KEY AUTOINCREMENT. The
remaining ~16 tables use TEXT PKs and stay 405 until a schema migration adds a
UUID-blob id column.
The withFileFallback pattern
src/lib/trailbaseRecords.ts wraps every Records API call in withFileFallback:
| HTTP status from TrailBase | What the helper does |
|---|---|
| 200/201/204 | Return the result |
| 404 / 405 / 501 | Log once, call the fileOp callback — same response across every pod, so it's safe |
| 5xx, network errors | Re-throw — falling back here would risk multi-pod split-brain |
The 4xx fallback is what lets the dashboard ship a route that prefers TrailBase but degrades to a file backend when the Records API doesn't (yet) serve the table. Most read routes are wrapped this way — that's why the dashboard's HTTP responses stay 200 even when TrailBase isn't exposing the underlying table.
Direct callers without fallback drop writes silently. statePoller and
sense/disturbances .upsertRecord(...) / .createRecord(...) straight into
TrailBase — when those tables 405, every write is lost (statePoller feed_status: 11/11 upserts failed in the dev log). Either wrap them, or fix the
schema PK and add the record_apis block.
The drizzle migration tracking re-sync recipe
Distinct from TrailBase but related — when the dashboard's drizzle-managed Postgres
tables (used elsewhere) crashloop with "X already exists", the fix is to bulk-insert
SHA-256 + when rows into drizzle.__drizzle_migrations. See the project memory
note for the full procedure.
What TrailBase is not used for
- Snapshot binary blobs. Those live in ReductStore.
- High-write OLTP beyond dashboard editing volume. TrailBase is SQLite under the hood; sized for operator edit traffic, not bulk telemetry.
- YAML configs.
holocron-config.yaml,archives-config.yaml,republic-config.yaml,nav-config.yaml,instances/registry.yaml, etc. stay in the repo.
See also
- ReductStore — sibling store for blobs
- Restreaming — primary table consumer
- Republic — primary table consumer