Where cms-admin stores registry, users, access-tokens, and other server-level state — and how to configure it for dev, Docker, Fly, and Kubernetes so it survives restarts.
The two kinds of data
cms-admin is a server. Sites it manages are clients. That line determines where each piece of data lives:
- Admin-server data — shared across every site: registry of orgs and sites, user accounts, access-tokens, push device tokens, beam transfer sessions, goto deep-link shortcuts, agent templates, org-level settings. None of this belongs inside any one site's folder.
- Per-site data — belongs to one site and travels with it: content JSON, site-config, team membership for that site, analytics, lighthouse reports, form submissions, brand voice, deploy log, scheduled events. Lives inside the site's own folder so moving the site to another server takes its state along.
This doc is about the admin-server half — where it goes and how to make sure it persists across restarts on every platform.
Resolution order
getAdminDataDir() checks these in order and uses the first one that resolves:
| # | Priority | Location | Used by |
|---|---|---|---|
| 1 | Override | $WEBHOUSE_DATA_DIR | Production — Docker, Fly, Kubernetes (set explicitly in Dockerfile / fly.toml / pod spec) |
| 2 | Auto-detect | /data/cms-admin/ if writable | Containers where the deploy layer has mounted a persistent volume there (no env var needed) |
| 3 | Linux standard | $XDG_DATA_HOME/webhouse-cms/ | Linux dev with XDG explicit |
| 4 | XDG default | $HOME/.local/share/webhouse-cms/ | Linux dev default |
| 5 | Simple fallback | $HOME/.webhouse/cms-admin/ | macOS + Linux developer machines |
| 6 | Legacy | {CMS_CONFIG_PATH-parent}/_admin/ | Pre-0.2.18 deployments only — auto-migrated on first boot with 0.2.18+ |
The override always wins. In production you should always either set WEBHOUSE_DATA_DIR explicitly or mount a persistent volume at /data/cms-admin — otherwise container restarts wipe the registry, users, and access-tokens.
Platform-by-platform setup
Local development (macOS, Linux)
Nothing to configure — cms-admin creates $HOME/.webhouse/cms-admin/ on first boot and writes there. To move it explicitly:
export WEBHOUSE_DATA_DIR=/opt/webhouse-cms
mkdir -p $WEBHOUSE_DATA_DIR
pnpm devDocker (plain)
The shipped Dockerfiles set WEBHOUSE_DATA_DIR=/data/cms-admin and expect a volume mount there:
docker run \
-p 3010:3010 \
-v $(pwd)/cms-admin-data:/data/cms-admin \
-v $(pwd)/my-site:/site \
ghcr.io/webhousecode/cms-adminWith docker compose, docker-compose.yml already declares a cms_admin_data named volume:
volumes:
- cms_admin_data:/data/cms-admin
volumes:
cms_admin_data:docker compose down followed by docker compose up reuses the volume; docker compose down -v wipes it.
Fly.io
deploy/fly.toml already attaches the cms_data volume at /data and sets WEBHOUSE_DATA_DIR=/data/cms-admin. Redeploy is safe — the volume outlives any one machine:
[env]
WEBHOUSE_DATA_DIR = "/data/cms-admin"
[mounts]
source = "cms_data"
destination = "/data"First-time volume setup (once per region):
fly volumes create cms_data --region arn --size 1
fly deployKubernetes
Use a PersistentVolumeClaim and mount it at /data/cms-admin. The container image auto-detects the /data/cms-admin path when writable, so you don't strictly need WEBHOUSE_DATA_DIR if you mount there — but it's a good habit to set it explicitly:
env:
- name: WEBHOUSE_DATA_DIR
value: /data/cms-admin
volumeMounts:
- name: admin-data
mountPath: /data/cms-admin
volumes:
- name: admin-data
persistentVolumeClaim:
claimName: cms-admin-dataBare-metal Linux (tarball install)
The weekly tarball contains code only — no runtime data. Install flow:
tar xf webhouse-cms-0.2.18.tar.gz -C /opt/
useradd --system --home /var/lib/webhouse-cms webhouse
mkdir -p /var/lib/webhouse-cms
chown webhouse:webhouse /var/lib/webhouse-cms
# /etc/systemd/system/webhouse-cms.service
[Service]
User=webhouse
Environment="WEBHOUSE_DATA_DIR=/var/lib/webhouse-cms"
Environment="CMS_CONFIG_PATH=/srv/site/cms.config.ts"
WorkingDirectory=/opt/webhouse-cms/packages/cms-admin
ExecStart=/usr/bin/node server.jsVerifying the configuration
After first boot, $WEBHOUSE_DATA_DIR should contain:
registry.json # all orgs + sites
_data/
users.json # user accounts
access-tokens.json # API tokens
device_tokens.json # push targets
invitations.json # pending invites
org-settings/
<orgId>.json
agent-templates/
<orgId>/
beam-sessions/ # cross-site beam transfers
beam-sites/ # beam-import staging
goto-links.json # /admin/goto/<id> short linksTo confirm cms-admin is reading from the right place:
curl -sk https://localhost:3010/api/cms/registry \
-H "Cookie: <your-session>" | jq '.registry.orgs | length'The count should match what's in $WEBHOUSE_DATA_DIR/registry.json.
What happens when the directory is ephemeral
If you run cms-admin in Docker/Fly/Kubernetes without mounting a volume at /data/cms-admin:
- First boot: cms-admin creates registry.json etc. in the container's local filesystem.
- You log in, create orgs/sites, mint tokens.
- Container restarts (redeploy, OOM kill, host reboot).
- Registry + users + tokens are gone. You're back to a fresh install.
The 0.2.18+ resolution order tries hard to catch this — auto-detecting /data/cms-admin when present, and falling back to $HOME otherwise — but a container with neither mount nor env var will still start, and will still lose data. Always verify your volume is mounted before inviting real users.
Migrating from pre-0.2.18
Before 0.2.18, admin-server data lived in {CMS_CONFIG_PATH-parent}/_admin/ and some files in {CMS_CONFIG_PATH-parent}/_data/. On first boot with 0.2.18+, if the new location is empty and the legacy one has a registry.json, cms-admin reads from the legacy path transparently — nothing breaks.
To migrate permanently:
mkdir -p $WEBHOUSE_DATA_DIR/_data
cp -R /path/to/bootstrap-site/_admin/. $WEBHOUSE_DATA_DIR/
cp /path/to/bootstrap-site/_data/users.json $WEBHOUSE_DATA_DIR/_data/
cp /path/to/bootstrap-site/_data/device_tokens.json $WEBHOUSE_DATA_DIR/_data/
cp /path/to/bootstrap-site/_data/access-tokens.json $WEBHOUSE_DATA_DIR/_data/
cp -R /path/to/bootstrap-site/_data/beam-sessions $WEBHOUSE_DATA_DIR/_data/
cp -R /path/to/bootstrap-site/.beam-sites $WEBHOUSE_DATA_DIR/beam-sitesAfter migration, leave the legacy paths in place for a release as a rollback safety net, then delete them once you've confirmed everything works.
See also
- Docker deployment — full Docker + Fly setup walkthrough.
- Deployment overview — all deploy targets.