Derive ownership on the server
A record ID identifies something; it does not prove who may read it. supastack scopes user-owned records by workspaceId. The server resolves the authenticated user’s workspace and includes that scope when reading or changing a record.
Do not treat a workspace ID sent by the browser as evidence of ownership. The browser can request an action, but the trusted server context must establish whose action it is. This rule needs to hold for task status, saved provider configuration, usage records, and every product-owned resource. Layouts and components can filter what renders, but rendering is not authorization — the check belongs where the record is actually read or written.
Everything else in this post follows from that rule: ownership is derived, never requested. The moment a client-sent workspaceId is trusted anywhere, every other check becomes decoration. The authentication post covers how the session itself is established upstream of this boundary.
Membership is part of the boundary
Scoping answers which workspace a record belongs to; membership answers whether this user belongs to that workspace. Resolve both on the server. supastack models personal and organization workspaces with owner, admin, and member roles — the workspaces guide documents the model.
Roles add a second axis: a member may read what only an admin may change. Keep the two checks distinct — “belongs to this workspace” and “may perform this action” — so a demotion or removal takes effect at the same boundary as everything else. Invitations and removals are part of the same lifecycle, not an admin afterthought.
Membership changes also have to take effect immediately. A removed member’s next request should fail at the boundary, not whenever a cached token happens to expire — which is another reason the check lives where records are read, rather than inside a credential issued earlier.
Make recovery useful without granting access
A task URL lets a user return after a refresh. It is useful precisely because it preserves a reference to the work. The authenticated API must still verify that the requested task belongs to the current workspace before returning status or results.
The same principle applies to submission recovery. An idempotency key helps identify a repeated request and prevent duplicate work. It is neither a session nor an access token. Retrying a saved submission still requires authentication, workspace ownership, and the appropriate entitlement.
Convenience paths are where isolation quietly erodes. A link that helps a legitimate user resume work — after a refresh, from a completion email — must pass through the same authenticated check as any other request. The request-to-result pipeline uses task-scoped idempotency keys on exactly this principle.
Test the requests that must fail
A test that lets an owner retrieve a record proves the happy path. It does not prove isolation. Pair it with a second workspace requesting the same record, an anonymous request, and a write that attempts to substitute another owner.
Assert the external behavior as well as the persisted result. A rejected request must not mutate another workspace’s data, and an error response must not leak secrets or raw database details. These checks make the boundary reviewable when implementation changes.
- A second workspace requests the same record ID — rejected.
- An anonymous request hits the same endpoint — rejected.
- A member removed from the workspace retries — rejected.
- A write names a different owner or workspace — rejected, with nothing mutated.
- Error responses stay generic — no secrets, no schema details.
Carry the rule into your product
When you add a new product table, decide its ownership before building the screen. Include workspaceId in its persistence model, resolve the workspace through the existing server boundary, and use that context in both list and detail operations.
Background workers need the same care. A queued task must carry enough trusted context to load the correct record, while provider credentials stay on the server. A scheduled job loading rows by bare ID is still inside the boundary. A consistent ownership rule is easier to maintain than a collection of route-specific exceptions.
The API surface belongs to the same boundary. Workspace API keys and signed outbound webhooks carry the workspace’s scope with them — a key must never become a way to read across tenants, and a webhook payload must never carry a neighbor’s records. The API module guide covers the shape of that surface.
Common questions about workspace isolation
Multi-tenant isolation raises the same handful of questions on every project. These are the ones worth settling early.
- Is application-level scoping enough, or do I need row-level security? — Server-enforced scoping on every read and write covers the threat model for most SaaS. Database RLS adds defense in depth but does not replace boundary checks — workers and API keys still need the same rule.
- How should sharing across workspaces work? — Model it explicitly — an invitation, a membership row — rather than weakening the scope. “Anyone with the link” should be a designed feature with its own checks, not an accident.
- What breaks first in multi-tenant apps? — Convenience paths: recovery links, admin tooling, background workers loading records by bare ID. Audit every code path that touches a record without going through the normal scope.
- How do I add scoping to an existing table? — Add
workspaceId, backfill it, enforce it in reads and writes, then touch the UI. Ship the negative tests first so the boundary is proven rather than assumed. - Do server actions need checks if the page is protected? — Yes. Page guards hide screens; they do not authorize operations. Every action resolves the session and workspace independently.