Platform

Multi-Tenancy

Every operation in Saaslivery is scoped to a workspace_id. There are zero exceptions.

Multiple workspaces — large enterprises, small startups, solo users — run on the same infrastructure. A workspace must never see, receive, or affect another workspace's data.


How workspace identity flows

The platform's auth middleware runs before any app code. It reads the session, extracts the workspace, and sets ctx.workspace_id:

# Platform middleware — runs on every request
async def enforce_workspace(req, res, ctx):
    session = await resolve_session(req)
    ctx.workspace_id = session.workspace_id  # read-only after this
    ctx.user = session.user

Apps read ctx.workspace_id — they never set it. The platform makes it immutable after initialization.


Isolation at every layer

1. Requests

ctx.workspace_id is set by platform middleware before any handler runs. Apps cannot bypass it.

2. Database

Every table includes a workspace_id column. The platform provides a scoped query helper:

# Apps use ctx.db — a ScopedDB already bound to the workspace
async def list_requests(req: Request, res: Response, ctx: Context):
    requests = await ctx.db.find("vacations_requests")
    # Only returns rows where workspace_id matches ctx.workspace_id

When you drop to raw SQL via db.raw, the scoping is your job: every query must carry WHERE workspace_id = ... explicitly.

-- Every app table follows this pattern
CREATE TABLE vacations_requests (
    id           UUID PRIMARY KEY,
    workspace_id TEXT NOT NULL,
    employee_id  TEXT NOT NULL,
    start_date   DATE NOT NULL,
    status       TEXT NOT NULL
);

CREATE INDEX idx_vacations_requests_workspace
    ON vacations_requests(workspace_id);

3. Events

The event bus only delivers events within the same workspace:

await req.app.emit("core.vacations.request.approved", {
    "workspace_id": ctx.workspace_id,   # platform overwrites this from ctx
    "data": { ... },
})

The platform overwrites workspace_id on every emitted event — apps cannot spoof it. Listeners in workspace A never receive events from workspace B.

4. Metering

Usage metering is auto-stamped with workspace_id:

await req.app.meter("minute", quantity=12)
# Platform internally: meter("minute", quantity=12, workspace_id=ctx.workspace_id)

5. WebSockets

Connections are scoped to workspace + user. A message in one workspace's chat is only delivered to that workspace's connected users.

6. File storage

All files are namespaced by workspace:

storage/
├── ws_facebook_abc123/
│   ├── files/
│   └── signatures/
├── ws_amazon_def456/
│   ├── files/
│   └── signatures/

The platform's file API enforces the workspace prefix.


What apps cannot do

Action Enforcement
Set or override ctx.workspace_id Immutable after platform middleware sets it
Emit events to another workspace Event bus overwrites workspace_id from ctx
Query data from another workspace ctx.db enforces workspace_id on all queries
Meter usage to another workspace Meter API injects workspace_id from ctx
Access another workspace's files Storage API enforces workspace path prefix
Send WebSocket messages across workspaces Connection pool is workspace-scoped

What the platform guarantees

  1. ctx.workspace_id is set before any app code runs
  2. ctx.workspace_id cannot be modified by app code
  3. All cross-cutting concerns are workspace-scoped — events, metering, storage, WebSockets, queries
  4. Third-party apps get the same isolation as core apps

Testing isolation

Workspace identity comes from the authenticated session, so isolation tests are written by logging in as members of different workspaces and asserting that data created in one is invisible in the other:

async def test_workspace_isolation(self):
    # Log in as a member of workspace A and create data
    self.login_as("owner@workspace-a.test", "password")
    _, res, _ = self.run_async(self.earth.POST(
        "/vacations", subdomain="api", headers=self.api_headers(),
        body={"start": "2026-04-01"},
    ))

    # Log in as a member of workspace B and list
    self.login_as("owner@workspace-b.test", "password")
    _, res, _ = self.run_async(self.earth.GET(
        "/vacations", subdomain="api", headers=self.api_headers(),
    ))
    assert res.json.data == []