Tenancy Model
1. Tenancy Goals and Threat Model
TestoQA is a multi-tenant system. Tenant isolation is a security requirement, not a convenience feature.
Goals
- Isolation: A user in Project A must never read or mutate Project B’s data.
- Correctness: All domain operations must occur within an explicit tenant context.
- Auditability: Tenant-scoped actions should be attributable to a user and project.
- Simplicity: Tenancy should be hard to accidentally bypass.
Threat Model (What we actively protect against)
- Missing
projectIdfilters in database queries - Cross-tenant access due to caching or shared keys
- Implicit tenant resolution that “guesses” incorrectly
- Using client-provided tenant state without server validation
- Logging/telemetry that accidentally leaks tenant-sensitive data
System-wide invariants are defined in
system-overview.mdx. This document focuses on tenancy specifics and enforcement.
2. Tenant Definition
What is a tenant?
A Project is the tenant boundary. The tenant identifier is:
projectId— the active tenant for the request
Tenant-scoped vs global data
- Tenant-scoped: any record that belongs to a project (must include
projectId) - Global: rare, explicit entities that are not owned by a project (e.g., auth/account primitives)
Rule: If something is tenant-scoped, it must have an unambiguous owner (projectId) and must be accessed through tenant-aware paths.
3. Tenant Context Resolution
Tenant context must be resolved at the boundary (Server Action / Route Handler) before doing work.
Sources of tenant context (allowed)
Tenant identity may be derived from:
- Route parameters (preferred: explicit in URL)
- Authenticated session (user memberships / defaults)
- Explicit user selection stored server-side (e.g., preference in DB/session)
What is not allowed
- “Guessing” tenant context based on partial information
- Trusting client-provided
projectIdwithout verifying membership - Falling back silently to “some project” when context is ambiguous
Resolution contract
A request is either:
- Tenant-resolved:
projectIdis present and verified for the user - Tenant-unresolved: request must fail fast (not proceed)
Fail-fast rule: If tenant cannot be confidently resolved, return an error and do no work.
4. Enforcement Rules
Hard requirements
- All tenant-scoped reads and writes must include
projectId. - Authorization must be evaluated per (user, projectId) before accessing tenant data.
- Tenant scoping must be enforced server-side (never in the client).
Where enforcement happens
- Boundary Layer: resolves tenant context and enforces authorization before orchestration
- Repositories: must not allow tenant-scoped data access without
projectId
5. Structural Safeguards
Because “forgotten filters” are a common failure mode, tenancy is enforced structurally rather than relying on developer memory.
Repository signature rule
Repositories should accept a RequestContext (or equivalent) that contains:
userIdprojectId
This makes tenant scope a required input to data access methods.
Query construction rule
Any query against tenant-scoped tables must be constructed so that projectId is injected into:
whereclauses for reads- write payloads for creates
- update/delete filters (never update/delete by id alone unless combined with
projectId)
Fail closed rule
If projectId is missing where required:
- treat it as a defect / invariant violation
- fail fast (do not “continue anyway”)
The exact mechanism (helpers, wrappers, middleware) is an implementation detail, but the architectural requirement is that tenant scoping cannot be accidentally omitted.
6. Caching and Revalidation Safety
Caching is a frequent source of tenant leaks.
Rules
- Avoid caching tenant-specific or authorization-dependent reads unless cache keys are explicitly scoped.
- Prefer
no-storefor sensitive or permission-dependent data. - Do not share caches across tenants unless the cache is provably tenant-agnostic.
Allowed caching
Caching can be safe when:
- the data is truly public / global, or
- the cache key includes tenant scope (and user scope if auth-dependent)
7. Failure and Misconfiguration Handling
Tenant unresolved
If the system cannot resolve tenant context, return a failure and stop.
Typical causes:
- missing route param
- user has no membership
- mismatched projectId / membership
Tenant resolved but unauthorized
Return an authorization failure before data access.
Tenant mismatch
If a request attempts to operate on a resource whose projectId differs from the resolved projectId, treat this as:
- unauthorized access attempt, or
- invalid request (depending on boundary semantics)
In all cases: do not return cross-tenant information.
8. Common Anti-Patterns (Do Not Do These)
- Query by id only on tenant-scoped tables (e.g.,
findUnique({ id })) without ensuring tenant filter semantics - Client-driven projectId used directly in server queries without verification
- Shared caching for tenant-scoped pages without tenant-aware keys
- Background work that runs without a tenant context
- Logging sensitive payloads that include tenant data (especially uploads, prompts, test artifacts)
9. Review Checklist (Tenancy)
When reviewing a PR that touches tenant-scoped data:
- Is
projectIdresolved at the boundary? - Is membership verified for the user?
- Are authorization checks performed before data access?
- Do all repository calls include tenant context?
- Do update/delete operations scope by
(projectId, id)(or equivalent)? - Is caching tenant-safe (or disabled)?
- Are logs tenant-safe and redacted?