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.
Recommended fix: GCS bucket mounted as a volume¶
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.jsonandimg/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¶
- Move content to a GCS volume and verify a save survives a redeploy and is visible from a fresh instance.
- Reserve the IP/cert for
cms.workwingman.ai, add the host rule toww-urlmappointing atww-site-backend. - Set
WINGCMS_ADMIN_HOST=cms.workwingman.aiandWINGCMS_ADMIN_KEY(a real secret, from Secret Manager) on the service. - Confirm
/Admin404s 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.