Content Management that can withstand the plundering hoards

Filesystem and persistence reference

Default roots are /cms (private) and /www (published). These are container mount paths, not directories an administrator must create on their workstation. The CMS writes both; frontend pods mount only /www, read-only. Both generated PVCs use ReadWriteMany. Separate mounts prevent public handlers from reaching private source or authentication credentials.

Private volume

/cms/
  project/
    project.json                 project identity and created/updated times
    settings.json                site/SEO/theme/layout settings, after first save
    pages/<page-id>.json         authoritative editable page documents
    components/plugin-<id>.json  installed renderer/block package documents
  assets/
    <asset-id>                   immutable uploaded binary bytes
    <asset-id>.json              filename, MIME, raw flag, size and update time
  templates/<theme-id>.json      custom theme source with encoded asset bytes
  drafts/                       reserved directory, not a separate page draft store
  working/                      mutation recovery and temporary publish snapshots

Bundled themes and sample pages are embedded in the CMS binary; custom themes are on the private PVC. Plugin documents include renderer bytes; they are not installed in the frontend. A saved draft is a page document in project/pages, not generated HTML and not a file under working/pages.

Page IDs and asset IDs are generated independently. A page JSON stores title, path, update time, content version/format, format-specific content and optional menu/layout/object metadata. See content and extensions for shapes. Settings are documented in site settings guide.

JSON saves use a same-directory temporary file, sync and atomic rename. Asset bytes commit before metadata, avoiding visible metadata referencing an absent file. Page mutations/branch deletes use recovery journals; startup and readiness recover pending transactions. Publication snapshots hold asset hard links under working/publish-snapshot-* until complete. Startup removes abandoned snapshots. Do not edit, prune or back up only selected private files while a writer is active.

Published volume

/www/
  release-sequence               durable last allocated release number
  activation-intent.json         prepared/latest activation for crash recovery
  activations.json               history of publish and rollback pointer moves
  releases/
    000001/
      index.html                 page path /
      about/index.html           page path /about
      api/assets/<id>/content     copied uploaded bytes
      api/assets/<id>/metadata.json  validated delivery MIME/filename metadata
      theme-assets/<sha256>/<filename>  selected theme images/fonts, when used
      sitemap.xml
      robots.txt
      release.json               completion marker and source revision
  current -> releases/000001

The CMS creates current.new and renames it over current on the same filesystem. The frontend uses /www/current, resolving the symlink again after its freshness interval; it does not bind a specific release using Kubernetes subPath. Releases are immutable after completion. release.json stores id, publishedAt, sourceRevision, result, previousRelease and description. The durable sequence prevents a cleaned-up release ID from being reused. Failed publications can leave gaps; gaps are not missing successful releases. IDs run from 000001 to 999999; exhaustion reports an error rather than wrapping or reusing IDs. Do not reset the sequence to work around exhaustion without an explicit migration of the entire release/history contract.

activations.json records pointer changes, including rollback, rather than mutating old release metadata. The intent record protects history across a crash around pointer activation. Check the active pointer and status after a late storage error: a reported error does not prove the pointer stayed unchanged. Do not manually rewrite sequence/history/intent to make an error disappear.

Every uploaded asset joins a snapshot, even if no page links to it. Public releases contain public data only. Renderer executable modules and auth Secrets are not copied into releases. Regular media and raw attachments preserve their same-origin API-shaped URL on the static site; it is a file path there, not a live API call. Older releases may lack asset delivery metadata until republished.

Storage guarantees and capacity

The driver must support symlinks, hard links within private storage, atomic rename within published storage, and file/directory sync. Verify behaviour on your actual RWX provider. A filesystem that reports sync errors cannot safely be assumed to have durably saved a release. Publication first syncs files and subdirectories, writes its durable completion/activation records and then moves the active pointer. See publishing for the release workflow.

The CMS runs as UID/GID 65532. The generated init container recursively chowns both volume roots using root plus CHOWN; it does not follow symlinks. A driver that denies chown, including some root-squash exports, needs storage-side preparation or policy changes before this init step can succeed.

Published storage needs room for the current release, configured history, a new in-progress release and temporarily retired releases. Media is copied into each release, so capacity can grow much faster than page text. Retention is current plus N most recent other releases after a 30-second grace period; cleanup is asynchronous and logged on failure. Snapshots temporarily retain private asset bytes even if the editor deletes the original reference.

Deleting a Website retains its PVC objects. This is distinct from a PV's persistentVolumeReclaimPolicy; deleting a PVC can still remove actual storage. Changing a Website's storage class/size is not a data migration procedure: existing claims may be immutable, expansion depends on the driver, so plan and validate storage changes with the platform administrator using storage-provider procedures. Use backup/restore for recovery instead of treating release history as source backups.