The frontend URL changes, a feature flag needs to go live, the logo gets swapped — and until now that meant touching an env var, rebuilding the container, restarting. RustyCMS separates operational settings from bootstrap secrets. Whatever changes during normal operation lives in _config/admin.json5 and takes effect without a restart. Whatever is secret — session secret, API keys, database URL — stays an env var and never touches disk.
Three ways in, one file
The admin UI renders a form at /admin/settings from the schema the server exposes at /api/config/_schema. Fields that only apply after a restart are marked as such. The same configuration is read and written via GET/PUT /api/config/admin, and for scripts and CI there is the CLI:
admin-config --content-dir ./content show
admin-config --content-dir ./content get frontend.site-a.baseUrl
admin-config --content-dir ./content set frontend.site-a.baseUrl '"https://www.example.com"'
admin-config --content-dir ./content validate
All three write through the same atomic path — temp file plus rename, no half-written state. A public subset is served at /api/config/public without login so the login page and the frontend can pick up branding and URLs; flags prefixed with _private. are stripped from it.
Changed on disk, live in the process
A file watcher observes _config/ and reloads the file within about 300 ms — whether it was changed by rsync, an editor or a deploy script. The hot reload only re-reads the file; it cannot escalate privileges or execute code.
Every change has a history
Before every save, the previous state is snapshotted to _config/.history/<timestamp>-by-<actor>.json5; the newest 50 are kept. In the admin UI a sidebar lists the snapshots: click one to preview it, “Roll back” restores it — and snapshots the current state first. The API offers the same through /api/config/history and /api/config/history/<id>.
Plugins bring their own section
A plugin registers a schema for its settings at boot and automatically appears as its own section in the form. Values are stored under plugins.<id> in the same file — no second configuration mechanism, no separate endpoint per plugin.
What stays an env var
Store mode, port, cache TTL, rate limits, backup directory: everything the process needs in order to start remains an env var or CLI flag. rustycms --help lists every variable with its default. Existing deployments that still set the preview configuration as an env var keep working: on the first start without admin.json5 it is picked up, and admin-config import-env writes it into the file in one step.