Skip to content

WingCMS hosting — editor subdomain and the persistence blocker

The blocker: cloud edits do not currently persist

ww-site runs on Cloud Run with maxScale=20, and the editor writes to the container's own filesystem (Content/site-content.json, wwwroot/img/team/*). That is fine on a laptop and broken in the cloud:

  • Edits reach one instance. A save lands on whichever of up to 20 containers served that request. Every other instance keeps serving the old copy, so the site appears to change or not change depending on which instance a visitor hits.
  • Edits die on deploy. The JSON and photos are baked into the image, and Cloud Run's filesystem is ephemeral. The next revision — or any instance recycle — silently reverts everything the team wrote.

Do not point editors at a cloud URL until this is fixed. Nothing about the editor code is wrong; the content store just has to live outside the container.

Cloud Run supports mounting a Cloud Storage bucket as a filesystem volume. Mount it at the content path and the application code does not change at all — it still reads and writes a file path, still hot-reloads, still validates.

  • Bucket holds site-content.json and img/team/*.
  • All instances see the same bytes, so an edit is immediately live everywhere.
  • Deploys no longer overwrite content; the image carries only a seed copy for first boot.
  • Object versioning on the bucket gives an undo history, replacing the audit trail git would have provided — without handing the request path git credentials, which is what the red-team review (RT-20/RT-21) told us not to do.

Caveat to verify during rollout: GCS FUSE is not a POSIX filesystem. The atomic temp-write-then-rename in ContentEditService and the photo staging path need a real test against a mounted bucket, and FileSystemWatcher will not fire on another instance's write — cross-instance freshness will likely need a short re-read interval or a pub/sub nudge instead of relying on the watcher.

Alternatives considered

  • maxScale=1 — fixes the split-brain but not the loss-on-deploy. Not enough.
  • Commit to git from the request path — rejected by the red-team gate; it puts repo write credentials in a public-facing process.

The editor subdomain

Recommend cms.workwingman.ai over admin.workwingman.ai: it describes what the thing is, and admin is the hostname every credential scanner tries first. Either works — the name is configuration, not code.

Set WINGCMS_ADMIN_HOST and the editor answers only on that host; on workwingman.ai and www.workwingman.ai every /Admin path returns 404 as if it did not exist. Leave the variable unset and the editor is reachable on any host, which is the convenient behaviour for local development.

Routing note: cms.workwingman.ai should point at the same ww-site backend, not a new Cloud Run service. A separate service would have a separate filesystem, so edits made there would never reach the public site. One service, two hostnames, different behaviour by host.

Rollout order

  1. Move content to a GCS volume and verify a save survives a redeploy and is visible from a fresh instance.
  2. Reserve the IP/cert for cms.workwingman.ai, add the host rule to ww-urlmap pointing at ww-site-backend.
  3. Set WINGCMS_ADMIN_HOST=cms.workwingman.ai and WINGCMS_ADMIN_KEY (a real secret, from Secret Manager) on the service.
  4. Confirm /Admin 404s on the apex and www hosts before telling anyone the URL.

Optional hardening once it is live: put Identity-Aware Proxy or an IP allowlist in front of the CMS host, so the shared editor key is a second lock rather than the only one.

Known limitation: concurrent editors across instances

The ETag guard is enforced per process. Two editors saving at the same moment on different Cloud Run instances can both read the same ETag and both write, so the later save wins and the earlier one's fields are lost. GCS FUSE makes this slightly worse than local disk because write visibility between instances is not immediate.

For a handful of trusted editors who mostly touch different sections this is a rare race, and no save is ever silently corrupted — the loser's change is simply not present. It is called out here rather than papered over. The fix, when the editor base grows: write through the GCS API with ifGenerationMatch preconditions so the store itself rejects the stale write, replacing the per-process lock with one the storage layer enforces. Tracked with the other v2 items in wingcms-v1-status.md.

How quickly edits appear

  • Text edits (copy, pricing, roles, badges): live within about 5 seconds. Each instance polls the content file's stamp on that interval and reloads when it moves.
  • Photo uploads: live within about a minute. The bucket mount caches file metadata, so a replaced image can serve stale bytes briefly even though the upload already succeeded. Measured on 2026-07-29: a swapped photo served the old bytes for well under two minutes, then corrected itself with no intervention.

Tell editors this up front. The failure mode to pre-empt is someone re-uploading the same photo three times because the first one "didn't work" — it did, it just had not surfaced yet. Nothing is lost by waiting.