{
  "slug": "admin-data-location",
  "title": "Admin data location",
  "description": "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.",
  "category": "deployment",
  "order": 5,
  "locale": "en",
  "translationGroup": "40c73852-3fdf-4d8b-8984-69255de0533b",
  "helpCardId": null,
  "content": "## The two kinds of data\n\ncms-admin is a **server**. Sites it manages are **clients**. That line determines where each piece of data lives:\n\n- **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.\n- **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.\n\nThis doc is about the **admin-server** half — where it goes and how to make sure it persists across restarts on every platform.\n\n## Resolution order\n\n`getAdminDataDir()` checks these in order and uses the first one that resolves:\n\n| # | Priority | Location | Used by |\n|---|----------|----------|---------|\n| 1 | Override | `$WEBHOUSE_DATA_DIR` | Production — Docker, Fly, Kubernetes (set explicitly in Dockerfile / fly.toml / pod spec) |\n| 2 | Auto-detect | `/data/cms-admin/` if writable | Containers where the deploy layer has mounted a persistent volume there (no env var needed) |\n| 3 | Linux standard | `$XDG_DATA_HOME/webhouse-cms/` | Linux dev with XDG explicit |\n| 4 | XDG default | `$HOME/.local/share/webhouse-cms/` | Linux dev default |\n| 5 | Simple fallback | `$HOME/.webhouse/cms-admin/` | macOS + Linux developer machines |\n| 6 | Legacy | `{CMS_CONFIG_PATH-parent}/_admin/` | Pre-0.2.18 deployments only — auto-migrated on first boot with 0.2.18+ |\n\nThe 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.\n\n## Platform-by-platform setup\n\n### Local development (macOS, Linux)\n\nNothing to configure — cms-admin creates `$HOME/.webhouse/cms-admin/` on first boot and writes there. To move it explicitly:\n\n```bash\nexport WEBHOUSE_DATA_DIR=/opt/webhouse-cms\nmkdir -p $WEBHOUSE_DATA_DIR\npnpm dev\n```\n\n### Docker (plain)\n\nThe shipped Dockerfiles set `WEBHOUSE_DATA_DIR=/data/cms-admin` and expect a volume mount there:\n\n```bash\ndocker run \\\n  -p 3010:3010 \\\n  -v $(pwd)/cms-admin-data:/data/cms-admin \\\n  -v $(pwd)/my-site:/site \\\n  ghcr.io/webhousecode/cms-admin\n```\n\nWith `docker compose`, `docker-compose.yml` already declares a `cms_admin_data` named volume:\n\n```yaml\nvolumes:\n  - cms_admin_data:/data/cms-admin\nvolumes:\n  cms_admin_data:\n```\n\n`docker compose down` followed by `docker compose up` reuses the volume; `docker compose down -v` wipes it.\n\n### Fly.io\n\n`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:\n\n```toml\n[env]\n  WEBHOUSE_DATA_DIR = \"/data/cms-admin\"\n\n[mounts]\n  source = \"cms_data\"\n  destination = \"/data\"\n```\n\nFirst-time volume setup (once per region):\n\n```bash\nfly volumes create cms_data --region arn --size 1\nfly deploy\n```\n\n### Kubernetes\n\nUse 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:\n\n```yaml\nenv:\n  - name: WEBHOUSE_DATA_DIR\n    value: /data/cms-admin\nvolumeMounts:\n  - name: admin-data\n    mountPath: /data/cms-admin\nvolumes:\n  - name: admin-data\n    persistentVolumeClaim:\n      claimName: cms-admin-data\n```\n\n### Bare-metal Linux (tarball install)\n\nThe weekly tarball contains code only — no runtime data. Install flow:\n\n```bash\ntar xf webhouse-cms-0.2.18.tar.gz -C /opt/\nuseradd --system --home /var/lib/webhouse-cms webhouse\nmkdir -p /var/lib/webhouse-cms\nchown webhouse:webhouse /var/lib/webhouse-cms\n\n# /etc/systemd/system/webhouse-cms.service\n[Service]\nUser=webhouse\nEnvironment=\"WEBHOUSE_DATA_DIR=/var/lib/webhouse-cms\"\nEnvironment=\"CMS_CONFIG_PATH=/srv/site/cms.config.ts\"\nWorkingDirectory=/opt/webhouse-cms/packages/cms-admin\nExecStart=/usr/bin/node server.js\n```\n\n## Verifying the configuration\n\nAfter first boot, `$WEBHOUSE_DATA_DIR` should contain:\n\n```\nregistry.json             # all orgs + sites\n_data/\n  users.json              # user accounts\n  access-tokens.json      # API tokens\n  device_tokens.json      # push targets\n  invitations.json        # pending invites\n  org-settings/\n    <orgId>.json\n  agent-templates/\n    <orgId>/\n  beam-sessions/          # cross-site beam transfers\nbeam-sites/               # beam-import staging\ngoto-links.json           # /admin/goto/<id> short links\n```\n\nTo confirm cms-admin is reading from the right place:\n\n```bash\ncurl -sk https://localhost:3010/api/cms/registry \\\n  -H \"Cookie: <your-session>\" | jq '.registry.orgs | length'\n```\n\nThe count should match what's in `$WEBHOUSE_DATA_DIR/registry.json`.\n\n## What happens when the directory is ephemeral\n\nIf you run cms-admin in Docker/Fly/Kubernetes **without** mounting a volume at `/data/cms-admin`:\n\n1. First boot: cms-admin creates registry.json etc. in the container's local filesystem.\n2. You log in, create orgs/sites, mint tokens.\n3. Container restarts (redeploy, OOM kill, host reboot).\n4. Registry + users + tokens are gone. You're back to a fresh install.\n\nThe 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.\n\n## Migrating from pre-0.2.18\n\nBefore 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.\n\nTo migrate permanently:\n\n```bash\nmkdir -p $WEBHOUSE_DATA_DIR/_data\ncp -R /path/to/bootstrap-site/_admin/.     $WEBHOUSE_DATA_DIR/\ncp /path/to/bootstrap-site/_data/users.json          $WEBHOUSE_DATA_DIR/_data/\ncp /path/to/bootstrap-site/_data/device_tokens.json  $WEBHOUSE_DATA_DIR/_data/\ncp /path/to/bootstrap-site/_data/access-tokens.json  $WEBHOUSE_DATA_DIR/_data/\ncp -R /path/to/bootstrap-site/_data/beam-sessions    $WEBHOUSE_DATA_DIR/_data/\ncp -R /path/to/bootstrap-site/.beam-sites            $WEBHOUSE_DATA_DIR/beam-sites\n```\n\nAfter migration, leave the legacy paths in place for a release as a rollback safety net, then delete them once you've confirmed everything works.\n\n## See also\n\n- [Docker deployment](/docs/docker-deployment) — full Docker + Fly setup walkthrough.\n- [Deployment overview](/docs/deployment) — all deploy targets.",
  "excerpt": "The two kinds of data\n\ncms-admin is a server. Sites it manages are clients. That line determines where each piece of data lives:\n\n- 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 short",
  "seo": {
    "metaTitle": "Admin Data Location — webhouse.app Docs",
    "metaDescription": "Where cms-admin stores registry, users, and access-tokens. Resolution order for Docker, Fly, Kubernetes, and local dev — with volume + env var setup.",
    "keywords": [
      "webhouse",
      "cms",
      "admin",
      "deployment",
      "docker",
      "fly",
      "kubernetes",
      "persistent volume",
      "data directory"
    ]
  },
  "createdAt": "2026-04-20T00:00:00.000Z",
  "updatedAt": "2026-04-20T00:00:00.000Z"
}