webhouse.appwebhouse.appdocs

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:

#PriorityLocationUsed by
1Override$WEBHOUSE_DATA_DIRProduction — Docker, Fly, Kubernetes (set explicitly in Dockerfile / fly.toml / pod spec)
2Auto-detect/data/cms-admin/ if writableContainers where the deploy layer has mounted a persistent volume there (no env var needed)
3Linux standard$XDG_DATA_HOME/webhouse-cms/Linux dev with XDG explicit
4XDG default$HOME/.local/share/webhouse-cms/Linux dev default
5Simple fallback$HOME/.webhouse/cms-admin/macOS + Linux developer machines
6Legacy{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:

bash
export WEBHOUSE_DATA_DIR=/opt/webhouse-cms
mkdir -p $WEBHOUSE_DATA_DIR
pnpm dev

Docker (plain)

The shipped Dockerfiles set WEBHOUSE_DATA_DIR=/data/cms-admin and expect a volume mount there:

bash
docker run \
  -p 3010:3010 \
  -v $(pwd)/cms-admin-data:/data/cms-admin \
  -v $(pwd)/my-site:/site \
  ghcr.io/webhousecode/cms-admin

With docker compose, docker-compose.yml already declares a cms_admin_data named volume:

yaml
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:

toml
[env]
  WEBHOUSE_DATA_DIR = "/data/cms-admin"

[mounts]
  source = "cms_data"
  destination = "/data"

First-time volume setup (once per region):

bash
fly volumes create cms_data --region arn --size 1
fly deploy

Kubernetes

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:

yaml
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-data

Bare-metal Linux (tarball install)

The weekly tarball contains code only — no runtime data. Install flow:

bash
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.js

Verifying 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 links

To confirm cms-admin is reading from the right place:

bash
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:

  1. First boot: cms-admin creates registry.json etc. in the container's local filesystem.
  2. You log in, create orgs/sites, mint tokens.
  3. Container restarts (redeploy, OOM kill, host reboot).
  4. 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:

bash
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-sites

After 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

Tags:ArchitectureDeploy: DockerDeploy: Fly.ioAccess TokensMigration
Previous
Cloudflare Pages — global edge, free
JSON API →Edit on GitHub →