Platform
Tenancy & security
eli.ai is multi-tenant: one deployment can host many isolated workspaces (a personal vault, several client engagements). Isolation is not a WHERE clause you have to remember — it is enforced by the database itself. This page is the load-bearing security contract.
The request path
Every tenant table is FORCE-RLS
Every tenant table carries a workspace_id (the leading index column) and a Postgres row-level-security policy. Critically, the policy uses FORCE ROW LEVEL SECURITY. Plain RLS is bypassed by a table's owner — and the app connects as the role that owns the tables — so without FORCE, every policy would be a silent no-op. FORCE makes the policy apply to the owner too.
Policies compare workspace_id against a transaction-local GUC and are written missing_ok, so when the context is unset they return zero rows (fail closed) instead of throwing or leaking.
withWorkspace() is the only door
All tenant DB access flows through one helper. It opens a transaction, sets app.workspace_id transaction-locally, and runs your callback under RLS:
Hard invariant: no I/O inside the callback
withWorkspace transaction. The pattern is always retrieve → commit → (call the provider) → short write transaction. Holding a pooled connection open across a 60-second model stream is the failure that freezes the app during a live demo.Identity comes from the URL, never the session
The workspace is always derived from the URL path (/w/[workspaceId]/…), validated against membership by requireWorkspaceAccess, which returns the caller's role and throws a WorkspaceAccessError (with a .status) otherwise. Session-derived scoping is banned: a consultant with two client tabs open would otherwise autosave client X's notes into client Y's workspace — and RLS would happily permit it, because the write is "correctly" scoped to the wrong tenant.
One user, many workspaces
Multi-tenancy isn't only for separate customers — the same user routinely holds several namespaces (a personal vault plus one workspace per client engagement), and the isolation contract between their own workspaces is exactly the same FORCE-RLS wall as between strangers: no document, entity, conversation, or connector ever crosses, and membership is checked per workspace on every request.
- Switcher— the sidebar header carries a workspace switcher listing every workspace you belong to (with your role), and a "create workspace" action that provisions a fresh, empty namespace you own.
- API — the switcher is backed by
GET /api/workspaces(the caller's workspaces: id, name, slug, role) andPOST /api/workspaces(create one, session-authenticated — this is control-plane data about the user, not tenant data, so it lives outside/api/w/:workspaceId). - Continuity — the active workspace is remembered in a cookie; the
/openentry point routes you back to your last-used workspace (membership-checked), falling back to your first. - Renaming — owners and admins rename a workspace under Settings → General.
The isolation canary
A permanent CI test seeds two workspaces and proves, against the real migrated schema as a non-superuser table owner, that:
- cross-workspace reads and writes are impossible;
- an unset workspace context reads zero rows (fail closed);
- a catalog check asserts every table with a
workspace_idhas bothrowsecurityandforcerowsecurity— so a new table physically cannot ship without RLS.
Any failure there is a release blocker.
How the pieces enforce it
- Agents & tools — the tool registry injects
workspaceIdinto every KB tool call; the model never supplies scope, so prompt injection can't cross workspaces. See Tools, MCP & HITL. - Providers — the per-workspace allowlist is enforced in the routing layer, so a residency guarantee is code, not convention. See Multi-provider AI.
- API keys— bearer keys are workspace-scoped and hashed; a key resolves to exactly one workspace, and a run against another workspace's data returns 404 under RLS. See Authentication.
- Instance-per-client — for contracts that forbid commingled data or backups, the compose topology makes a dedicated instance cheap.