The short answer: put browser-accessible data beneath a tenant-scoped path, make every allowed operation prove that the signed-in user belongs to that tenant, and test both the permitted and denied paths in the Firestore Emulator. Do not rely on hiding a tenant ID in the client or on a client-side filter. A single overly broad rule can turn a shared collection into a cross-tenant read.
This guide is for a Next.js-capable solo developer whose SaaS is moving from one user per account to customers with shared accounts. The immediate problem is keeping a valid session from becoming permission to read another customer’s documents. The desired outcome is a small, auditable rule set whose tenant boundary survives direct document reads, queries, creates, and updates.
Start with the boundary, not a role name
Tenant isolation is primarily a data-access question: which tenant owns this document, and how does the request prove membership in that tenant? Roles can refine what a member may do, but a role check without a tenant boundary is not isolation.
A practical starting shape is to keep tenant-owned documents below their tenant document:
tenants/{tenantId}
members/{userId}
projects/{projectId}
invoices/{invoiceId}
That path gives Rules the tenant identifier directly. It also makes the visual and query boundary easy to review: a browser request to one tenant’s projects should never need a broad collection query and a client-side filter.
There are other valid data models, including top-level documents with an immutable tenantId. Choose one model deliberately, then make your queries prove the same constraint that your Rules expect. Firestore evaluates a query against its potential result set; Rules are not row filters that remove unauthorized documents after a broad query has run. Firebase’s secure-query guidance explains this all-or-nothing behavior.
A deny-by-default tenant pattern
The following is an illustrative Rules pattern, not a drop-in authorization system. It assumes that trusted server code creates membership documents and that browser clients need project access. Review field names, ownership, mutation policy, and deletion behavior for your own product before using it.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isTenantMember(tenantId) {
return signedIn() &&
exists(/databases/$(database)/documents/tenants/$(tenantId)/members/$(request.auth.uid));
}
match /tenants/{tenantId}/projects/{projectId} {
allow get, list: if isTenantMember(tenantId);
allow create: if isTenantMember(tenantId)
&& request.resource.data.tenantId == tenantId
&& request.resource.data.createdBy == request.auth.uid;
allow update: if isTenantMember(tenantId)
&& request.resource.data.tenantId == resource.data.tenantId
&& request.resource.data.tenantId == tenantId
&& request.resource.data.createdBy == resource.data.createdBy;
allow delete: if false;
}
}
}
The important parts are the relationship, not the exact collection names. The path supplies tenantId; the membership document supplies the authorization fact; the write checks prevent a client from assigning a different tenant or silently changing the creator. Everything without a matching allow remains denied.
Firebase documents that Rules conditions can inspect authentication, the requested document, the proposed document, and other documents through get() or exists(). Those access calls are limited and can be billed even when a request is rejected, so keep membership lookups simple and reuse a focused function rather than constructing a large rules-based policy engine. See Firebase’s conditions documentation.
What this pattern does—and does not—protect
It protects client-SDK calls evaluated by Firestore Rules. A signed-in member of tenant A can read tenant A’s project path; a user without that membership cannot. A client cannot create a project claiming another tenant, and it cannot alter the tenant ID or original creator in this example.
It does not protect privileged server access. Firebase states that server client libraries, including Admin SDK access, bypass Firestore Security Rules and authenticate through Google credentials instead. That means a Next.js Route Handler, Server Action, background job, or webhook using Admin SDK access needs its own server-side authentication and authorization checks. Treat Rules and server authorization as two separate boundaries, not interchangeable backups. Firebase documents the bypass explicitly.
This distinction matters for architecture. Frontend Accelerator’s current repository keeps database provider access behind adapters rather than letting features import Firebase directly. That is a useful place to centralize a server-side tenant assertion if you add tenant support, but it is not evidence that the product currently provides organization-based multi-tenancy. It does not.
Make queries carry the tenant boundary
With documents under tenants/{tenantId}/projects, the client should query that exact subcollection. With a shared top-level collection, the query must constrain tenantId so that every possible result satisfies the Rule. A broad query followed by where in application memory is both the wrong security model and likely to be rejected by Firestore.
Write the expected query next to the rule during review. For example: “a tenant member may list projects only by querying this tenant’s project path.” That one sentence catches a common mismatch: a rule correctly requires a tenant field, but the UI still asks Firestore for every project.
Do not let writes move documents across tenants
Read checks alone are not enough. On create, compare the proposed tenant ID to the path. On update, compare the proposed value with the existing document and the path. For fields such as ownerId, createdBy, plan status, or billing flags, decide explicitly whether a client may change them. A safer first version denies deletion and delegates privileged lifecycle operations to reviewed server code.
Test the isolation contract in the Emulator
Rules deserve tests that express the product contract, not just a manual console check. Firebase’s Rules unit-testing library can create authenticated and unauthenticated contexts against the local Emulator, while setup data can be loaded with Rules disabled. That makes both positive and negative cases repeatable. Firebase’s Emulator testing guide shows the initializeTestEnvironment, authenticatedContext, assertSucceeds, and assertFails workflow.
Original asset: tenant-isolation test checklist
- Unauthenticated read: a request for any tenant project fails.
- Member read: user A, seeded as a member of tenant A, can get and list tenant A projects.
- Cross-tenant read: user A cannot get or list tenant B projects.
- Constrained query: the exact UI query succeeds only when it targets tenant A’s path or otherwise proves tenant A.
- Forged create: user A cannot create a project under tenant A while setting
tenantIdto tenant B orcreatedByto another user. - Tenant move: an update attempting to change
tenantIdfails. - Membership mutation: a normal member cannot create, edit, or delete membership documents unless you have deliberately designed and tested that workflow.
- Privileged path: server-side code has its own authentication and tenant-authorization tests because Admin SDK calls bypass Rules.
The failure cases are the asset here. A test that only proves “the owner can read” does not show that an accidental cross-tenant path is denied.
Failure modes to review before launch
- Open fallback matches: a broad recursive matcher or an
allow read, write: if request.auth != nullrule can silently override a careful tenant matcher. - Membership in client-controlled data: if a user can write their own membership document, the membership check no longer proves authority.
- Rules that assume filters: a query that could return another tenant’s data is rejected; do not expect Rules to trim it.
- Server trust leakage: accepting a client-provided
tenantIdin a Route Handler without resolving membership on the server recreates the authorization bug outside Rules. - Untested changes: a schema change, new collection, or collection-group query can introduce an uncovered path even when existing tests pass.
When this approach fits
Use tenant-scoped client access when your product benefits from direct browser reads or realtime listeners and you can keep the authorization model small and testable. Avoid pretending that this pattern is complete enterprise authorization: invitations, per-tenant roles, delegated administration, support access, audit requirements, and billing entitlements need additional product and security design.
For a solo SaaS, the practical win is narrower: make the tenant boundary obvious in paths, Rules, queries, and tests before shared accounts reach production. Once that contract is explicit, your Next.js server code and client access layer have something concrete to preserve.
One release review worth doing
Before deploying a new browser-readable collection, write a denial test for its nearest plausible cross-tenant path and query. Then add the collection to the rules review checklist. This is more dependable than assuming an existing tenant matcher covers a new nested collection, collection-group query, or client listener.



