Přeskočit na obsah

Where to put the meta store

Tento obsah zatím není dostupný ve vašem jazyce.

Adminium keeps its own state — users, roles, connections, snapshots, page config, saved views, audit log, jobs — in tables prefixed adminium_*. That is the meta store, and it is a different thing from the database you are building an admin panel for.

Where it lives is the most consequential self-hosting decision you make, and the most annoying to change later.

Can Adminium's data role WRITE, and does it have DDL?
│
├─ NO (read-only role, or no DDL)
│ │
│ └─► A SEPARATE meta store is MANDATORY.
│ Adminium enforces this: same-database placement is refused
│ with META_PLACEMENT_INVALID at connect time.
│
└─ YES
│
├─ Production, or more than one user?
│ │
│ ├─ YES ─► SEPARATE DATABASE. ← recommended
│ │ Adminium's migrations never touch your data server.
│ │ Independent backup schedules. Independent blast radius.
│ │
│ └─ NO ──► Same database, dedicated schema. Acceptable.
│ One thing to back up — and one thing to lose.
│
└─ Just looking? ─► Embedded SQLite (the default).
It warns at every boot. Believe it.

A read-only data role means the meta store cannot live in that database.

Adminium refuses same-database placement against a read-only or DDL-less role, with META_PLACEMENT_INVALID, at the first instant both facts are known: right after the capability probe, before anything is stored.

The alternative — accepting the configuration and failing at first write — is a setup that looks successful and collapses when someone tries to log in.

Nothing else could work. Adminium must write its own tables; a role that cannot write cannot host them. See Read-only sources & the meta database for the full three-connection story.

A file under ADMINIUM_DATA_DIR. Zero configuration; it is what makes a fresh adminium install work with nothing but a secret.

It warns at every boot. That is deliberate: this fallback must never become your production meta store by accident.

Good for Bad for
A laptop, an evaluation, a demo More than one user
The desktop app Anything on a host you might lose
CI Anything you would be upset to re-create by hand

If you use it, the data directory is your database. Back up ADMINIUM_DATA_DIR as a database, not as a cache.

-- PostgreSQL
CREATE SCHEMA adminium AUTHORIZATION adminium_rw;
-- MySQL: a separate database on the same server
CREATE DATABASE adminium_meta;
GRANT ALL PRIVILEGES ON adminium_meta.* TO 'adminium_rw'@'%';
Terminal window
ADMINIUM_META_URL='postgres://adminium_rw:pass@your-db:5432/mydb'

Requires DDL on the data connection.

For: one backup covers everything. One database to operate. Genuinely simpler.

Against: Adminium’s migrations run against your production data server. Adminium’s load lands on your production data server. A restore of your data database rolls back your Adminium config with it — including your audit log, which is exactly the thing you want to survive an incident.

Section titled “A separate database — recommended for production”
Terminal window
ADMINIUM_META_URL='postgres://adminium:pass@meta-host:5432/adminium_meta'

The meta store’s engine is independent of your source engine. A MySQL source with a Postgres meta store is normal and fully supported. Adminium’s tables carry their own schema and their own migration ledger.

For: blast radius. Adminium’s migrations never touch your data server; your data server never carries Adminium’s load; the two back up and restore independently; a compromised source connection does not imply a compromised meta store.

Against: one more database to run. That is the whole list.

With Docker Compose, --profile with-meta gives you one: Docker Compose.

Managed Postgres shows you a pooled connection string first — Neon’s -pooler host, Supabase’s port 6543, Fly’s pgbouncer. For the meta store that is fine. Adminium takes its migration lock with pg_try_advisory_xact_lock inside one explicit transaction, and a transaction-mode pooler keeps a transaction on one backend from BEGIN to COMMIT, so the lock and the migration pass it guards stay together. Two replicas booting into the same pending migrations still serialize.

This is a real constraint and not a theoretical one: a session-scoped lock behind the same pooler is held by whichever backend served the select, while the migrations run on whichever backend serves them next — so both replicas would be told they hold the lock, and both would migrate.

MySQL is different, and the difference is not fixable. GET_LOCK is session-scoped, MySQL has no transaction-scoped equivalent, and it has no transactional DDL to hang one on — which is the reason the lock exists at all there. A transaction-pooling proxy in front of a MySQL meta store cannot serialize migrations. Point ADMINIUM_META_URL at the direct endpoint.

One thing to size either way: each Adminium process opens up to 10 meta-store connections, so a fleet of n processes wants headroom for 10n.

“But my source database is SQLite.” Then you have two SQLite files with two jobs: your application’s database, which Adminium reads, and Adminium’s meta store, which Adminium writes. Unrelated files. Do not point them at each other.

“Can’t I just back up the meta store?” Back it up, yes — but a meta-store backup is unreadable without ADMINIUM_SECRET, since every stored DSN and API key in it is encrypted under a key derived from that secret. Back up both, and test the restore before you need it.

It is possible and it is work: dump the adminium_* tables, restore them into the new store, point ADMINIUM_META_URL at it, keep the same ADMINIUM_SECRET — or every encrypted value moves across as ciphertext you can no longer read.

Cheaper to decide correctly now. If you are past “just looking”, use a separate database.