The security model

Most security incidents on multi-tenant platforms are not exotic. They are one tenant reaching another’s data, one missing check on one endpoint, one piece of user content that turns out to be executable. CTXR is designed so that the common failure modes are structurally impossible rather than carefully avoided. This page explains how.

A space is a boundary

A space is a directory, and everything inside it belongs to that space alone. There is no shared database, no global user table, and no query that spans spaces. The boundary is physical, not a WHERE clause that someone could forget.

This matters most at the front door. When a request arrives, the platform resolves which space it belongs to from the subdomain, before anything else runs. There is no window in which a request exists without a space attached, and therefore no “what if someone crafts a request that crosses tenants” — the request handler has already decided whose space this is by the time any logic executes. Because a space is just a directory, this also means an editor or key from one space is simply unknown to another, and error messages are written so they don’t reveal whether content in another space exists. The fact that everything is file-based is what makes this the foundation every other protection sits on.

Layered authorization

Security at CTXR is a sandwich, not a single lock. Authentication establishes who you are. Permissions decide what you may do. Property protection governs which fields you may touch. These gates are independent and checked separately, so breaking one is not enough to get through — an attacker who somehow forged a session still faces the permission check, and an actor with the right permission still cannot write protected fields.

Sign-in without passwords

Editors sign in with a six-digit code sent to their registered email. The code is single-use and short-lived; after verification the editor holds a session bearer token. External systems authenticate with a cryptographically random WebAPI token. There are no passwords anywhere to store, hash, rotate, or leak. Because the code arrives on a separate channel, the flow is already two-factor in spirit.

Ten atomic permissions

Access control uses ten granular rights that cover every operation the platform supports:

Permission Scope
view Read nodes, query the graph
write Create and edit drafts
publish Approve drafts to live
delete Remove nodes
create.route Create routable pages (with a URL path)
create.collection Create embedded content (blocks)
pin Curate content (add references)
submit Public form submissions
manage.actors Create and edit persons and API keys
manage.settings Space configuration and worker operations

Presets bundle these into familiar roles so you rarely assemble arrays by hand — an editor typically holds view, write, publish and the creation rights; a contributor holds view, write and submit; a viewer holds only view. Presets are a convenience; you can always grant a custom combination when none fits. Either way, the array is what gets checked — at every mutation point, inside the service layer as well as at the API edge, and once more in templates, where editing controls render only when the actor’s authenticated state (not a URL parameter) says they may edit. A key with only view can read the whole graph and change nothing.

Actors are symmetric

A person, a bot, and an API token are the same kind of thing: an actor with a permissions array. A bot does not get a back door or a softer rule set; it gets exactly the rights you grant it and a richer audit trail because automated activity is easy to attribute. This symmetry keeps the model small — there is one authorization story, not one for humans and a quieter one for machines.

Content that cannot be tampered with

Every node records who created it and who last edited it. These are set by the platform, never by the submitter. On every write path the incoming data is stripped of @-prefixed and internal fields, and of creator and editor values, before any logic runs — so a crafted request cannot impersonate another actor or backdate content. The creator and editor fields are also removed from public output, preventing actor enumeration.

Rich text is safe by construction rather than by sanitizing. Editors produce a structured document model, and the platform renders it through a trusted generator. There is no point at which raw HTML from a user is trusted and then scrubbed — the dangerous path simply does not exist, which closes off the most common cross-site-scripting vector in content platforms.

A hardened API

The editing API is locked down regardless of how a space is configured. Cross-origin requests are accepted only from the platform’s own domain family; everything else is rejected by the browser before it reaches your space. Request bodies are size-bounded and rate-limited. Uploads are restricted to a curated whitelist of image, audio, video, and document types, and SVG is blocked outright because it can carry executable script.

These protections are always on. A multi-tenant platform cannot rely on every space owner configuring security correctly, so the defaults are the safe ones and there is no switch that turns them off.

Making a whole space private

By default a space’s live content is public. Turning on view gating in the space’s settings flips that: every live page then requires the view permission, so only signed-in actors can see anything. This turns a space into a private site — useful for internal documentation, a client portal, or a staging environment that isn’t ready for the world. View gating governs pages; raw uploaded files remain reachable by URL (see Privacy and GDPR).

What CTXR deliberately does not do

Being honest about the edges matters as much as listing the protections. A few things are out of scope by design rather than by oversight:

  • No encryption at rest of stored files. Isolation, edge gating and minimal stored data carry the load; a key-management layer would add operational weight without changing the threat model for a content platform. Don’t treat uploads as a secure vault.
  • No per-node version history. A node has a draft and a live version, not a full revision log. Soft-delete gives you a window to recover a deletion, but it is not an audit of every past edit.
  • No second factor beyond email. The platform does not layer its own MFA on top of the email code, which is why hardening the editor’s mailbox is the controller’s job (covered under Privacy and GDPR).

Knowing where the line sits lets you put the right compensating control on the other side of it.