Skip to content

Isolation model

The wrapper’s isolation model is a four-level hierarchy. Each level narrows what a caller can see one notch further than the level above it. Understanding these four levels — and, crucially, which of them is enforced by a token rather than by a shared key — is the key to the whole system.

Isolation model: a project contains an extractable integration key, but the real boundary is the subject-bound user (Master) token, under which each space (Ruta) gets its own isolated partition.

A project is a tenant. Everything else belongs to exactly one project, and the project id is the outermost partition: no query ever crosses it. A project is created by an admin and receives a PROJECT_API_KEY (returned exactly once). The project owner uses that key — or a portal login (a project-role JWT) — to manage the project: keys, integrations, plans, users, and spaces.

Every authenticated request resolves to a project_id, and that id is injected into the request context and folded into every database query. This is the baseline multi-tenant guarantee and it is unconditional.

An integration represents a client application that talks to the wrapper on behalf of the project: a mobile app, a web front-end, an internal tool. Each integration has its own INTEGRATION_API_KEY, its own declared scopes, and its own rate-limit / CORS policy.

The critical property: an integration key identifies an app, not a user. It answers “I am the Liquida Pro mobile app” — nothing more. It is deliberately not an isolation boundary between the app’s own users, because the key lives inside the app build and is extractable.

3. User (“Master”) — the end-user identity

Section titled “3. User (“Master”) — the end-user identity”

A user is an end-user of the integration’s app. In the Liquida domain this is a Master: a paying customer who owns files and manages a set of workspaces. Users are provisioned by the integration (or the project owner) and carry both an internal id and an integrator-supplied external_id.

A user becomes a caller by logging in: the integration exchanges its key for a short-lived user token bound to that user’s id. From that point, the token — not the integration key — is what the user’s client presents, and it can only ever act on that one user’s data.

4. Space (“Ruta”) — one level below the user

Section titled “4. Space (“Ruta”) — one level below the user”

A space (domain term: Ruta) is an isolated storage partition underneath a Master. A Master can own many Rutas, and each Ruta is blind to the others. This models the Liquida case where one Master (e.g. a company) has many field workers, each of whom must see only their own workspace.

  • A file with space_id IS NULL is a Master-level file, owned by the user directly.
  • A file with space_id = X belongs to Ruta X.

Visibility follows the hierarchy:

Caller Sees
Project / integration key project-wide (all users, all spaces)
User (Master) token own files (space_id IS NULL) plus all its spaces’ files
Space (Ruta) token only files in its bound space_id

A Ruta is also a login identity: a worker signs in with the Ruta’s own external_id + password, and the wrapper mints a space token bound to that single space. A space may be active on one device at a time (last-wins).

Why the token is subject-bound, not the key

Section titled “Why the token is subject-bound, not the key”

This is the security crux of the entire design.

Both integration keys — the Master app’s and the Pro app’s — ship inside app builds. A key baked into a mobile build can be pulled out of the APK in minutes. If that key were the isolation boundary, extracting it would hand the attacker every user’s and every workspace’s files across the tenant.

So the wrapper makes the login the boundary instead:

  • Master login mints a user token (subject = the Master’s user_id, no space).
  • Ruta login mints a space token (bound to a single space_id; its owner user is carried along so the subject guard still applies).

The token is:

  • Short-lived — user tokens default to 15 minutes (USER_TOKEN_TTL_SECONDS), space tokens to 1 day (SPACE_TOKEN_TTL_SECONDS); the integration re-mints.
  • Subject-pinned — a hard guard (not a warn-only check) rejects any request that targets a different user or space, no matter what scopes the token holds.
  • Revocable — every token carries a jti; revoking blocklists it.

Minting authority is itself partitioned as defense in depth: the Pro integration can mint space tokens only, the Master integration mints user tokens. A stolen Pro key can therefore never mint a Master token — the worst it can do is what the Pro app itself was allowed to do.

Concept Where it lives
Unified caller identity apps/api/src/lib/principal.ts (Principal, subjectUserId, spaceId)
Session roles apps/api/src/lib/session.ts (project / user / space)
Subject pinning apps/api/src/middleware/subject-guard.ts
Space visibility filter file query keyed on principal.spaceId in apps/api/src/routes/files.ts
spaces table + files.space_id apps/api/src/db/schema/spaces.ts, .../files.ts

The design rationale is captured in docs/proposals/scoped-spaces-isolation.md and its predecessor docs/proposals/per-user-access-isolation.md.

Next: see how each of these levels becomes a distinct authorization axis with its own guard in Authorization axes.