How to Model a Multi-Tenant SaaS in Firestore

Published on July 20, 2026 · 8 min read

How to Model a Multi-Tenant SaaS in Firestore

For most early B2B SaaS products, the clearest Firestore model is one tenant document, membership records beneath it, and tenant-owned data beneath the same tenant boundary. Make every request resolve a tenant and prove membership before it reaches tenant data. A tenantId field alone can work for a narrow collection, but it is not a tenancy model unless your queries, Security Rules, server code, and tests all enforce it.

This guide is for a React and Next.js-capable solo developer moving from personal accounts to shared customer accounts. The immediate problem is avoiding a data model that looks multi-tenant in the UI but can leak, mis-query, or become painful to extend. The desired outcome is a small schema whose ownership boundaries remain obvious when you add projects, billing, and roles later.

Start with the tenant as the ownership boundary

A tenant is the customer account that owns shared application data. It might represent a company, a client account, or a team. A user is a person who can belong to one or more tenants. Those are different entities, even if your first customer has one user.

Cloud Firestore stores documents in collections, supports subcollections, and lets same-named subcollections be queried with collection group queries. That makes a tenant root and tenant-scoped subcollections a natural starting point, but it does not automatically authorize access. You still need to make the path, membership lookup, client query, and rule condition agree.

A practical collection structure for a small SaaS

Use this as an illustrative starting point, not as a universal schema or a copied Accelerator implementation:

users/{uid}
email
displayName

tenants/{tenantId}
name
createdAt
plan

tenants/{tenantId}/members/{uid}
role // owner | admin | member
joinedAt

tenants/{tenantId}/projects/{projectId}
name
createdBy
createdAt

tenants/{tenantId}/projects/{projectId}/tasks/{taskId}
title
status
assigneeId

The important decision is not the nouns. It is the path: a project is owned by exactly one tenant because it lives under that tenant’s document. A membership record uses the authenticated user’s ID as its document ID, so a rule can look up a known, bounded path instead of scanning for a matching membership.

Why this structure is useful

  • Ownership is visible: a project path includes the tenant that owns it.
  • Membership is explicit: a person can be a member of several tenants without duplicating their profile.
  • Tenant-scoped reads stay narrow: the client reads one tenant’s projects rather than a global collection and filters in the browser.
  • Rules can follow the path: the same tenantId used in the URL and query is available in the rule match.

Firestore documents are lightweight and subcollections can model a hierarchy. Remember the deletion consequence: deleting a parent document does not delete its subcollections. A tenant-deletion workflow therefore needs an explicit, privileged cleanup process and a test for partial failure; it is not a single-document delete.

Keep membership and data checks on the same path

The core access question is simple: “Is the signed-in user a member of this tenant?” Put that answer in one canonical place, then reuse it for every tenant-owned collection. The following Rules sketch is illustrative. It must be adapted and tested against your actual paths, custom claims, and write model.

service cloud.firestore {
match /databases/{database}/documents {
function isMember(tenantId) {
return request.auth != null &&
exists(/databases/$(database)/documents/
tenants/$(tenantId)/members/$(request.auth.uid));
}

match /tenants/{tenantId} {
allow read: if isMember(tenantId);

match /projects/{projectId} {
allow read: if isMember(tenantId);
allow create: if isMember(tenantId)
&& request.resource.data.createdBy == request.auth.uid;
}
}
}
}

This is deliberately deny-by-default: it does not grant a broad authenticated-user rule and it does not let a client choose an arbitrary tenant merely by sending an ID. For writes, define separately who can add members, change a role, update a project, or delete tenant data. A member check is not a complete role model.

Security Rules are also not filters. A client query must be constrained so its possible result set satisfies the rule; asking for a broad collection and expecting Firestore to remove other tenants’ documents is not safe or valid. Test the expected denial: an authenticated user from tenant A must not read, create, update, or delete a project path in tenant B.

Decide where each query belongs

Begin with the product screens you actually need. Most customer-facing routes should already know the active tenant from the URL, session, or a server-side selection. That gives each query a concrete path:

  • Tenant dashboard: read tenants/{tenantId} after resolving membership.
  • Project list: query tenants/{tenantId}/projects, then paginate and order within that tenant.
  • Cross-tenant internal reporting: consider a collection group query only when an administrator genuinely needs it. It may require an index and, for web or mobile clients, rules that permit the collection group query.

Do not build an operations query by quietly reusing a customer query. Cross-tenant administration is a separate authorization surface. In many apps it belongs in trusted server code with IAM, auditing, and a deliberately small result set. Firebase’s server client libraries bypass Firestore Security Rules, so server handlers still need their own tenant and role checks.

When a top-level collection is the better trade-off

A flat projects/{projectId} collection with a required tenantId can be reasonable when cross-tenant operations are central and every query consistently filters by tenant. It trades path clarity for more deliberate query and rule discipline. If you choose it, make tenantId immutable after creation, include it in every customer-facing query, and write Rules that require the query’s tenant constraint.

For a first shared-account SaaS, nested tenant data is usually easier to review because ownership is encoded in the path. Move to a flatter or denormalized read model only when a measured query need makes that trade-off worthwhile. Denormalized copies create another invariant: every copy must remain scoped to, and synchronized with, the same tenant.

Do not confuse product roles with tenant roles

Frontend Accelerator currently provides global admin and member roles with role-aware navigation and server-protected administrator routes. Its database layer selects a Firestore or MongoDB adapter behind a DatabaseAdapter. Those are useful application boundaries, but they are not an organization model. Tenant membership, per-tenant roles, invitations, and tenant-scoped rules remain product-specific work to design and test.

That distinction is useful when planning the extension. Keep provider SDK calls behind the existing database adapter boundary, put product-specific tenancy behavior in a feature module, and keep the active tenant resolution on the server for protected actions. Do not let a client-side tenant switcher become the authority for access.

Make the active tenant an explicit server-side input

A route such as /app/[tenantSlug]/projects can make the selected tenant visible to the reader and to the server. The server should resolve tenantSlug to a tenant ID, load the current user’s membership, and reject the request before it invokes a project repository. Passing a raw tenantId from a client component directly into a privileged Server Action is not enough; treat it as untrusted input and re-check membership inside the action.

For creation, decide which invariants are written together. A new tenant often needs the tenant document, its first owner membership, and perhaps a billing-owner reference. If one succeeds without the others, the application can leave an orphaned account or an account with no administrator. Use a transaction or an explicit recovery path appropriate to the workflow, then write a test for the partial-failure case. Firebase documents that rules can use getAfter() to inspect the post-transaction state when you need to validate related writes, subject to the documented access-call limits.

Keep the model explainable in one sentence: “This user may access this project because they have this membership under this tenant.” If you cannot say that without referring to UI state, an unscoped global query, or an undocumented server exception, the boundary is not ready for customer data.

A pre-build tenant isolation checklist

  1. Name the entity that owns shared data: company, client account, or team.
  2. Choose one canonical membership path and document the role values it stores.
  3. Write the tenant path for every customer-facing collection before adding the UI.
  4. List each query and state whether it is tenant-scoped or trusted internal administration.
  5. Write deny tests for a user from another tenant, a signed-out request, and a role that lacks write permission.
  6. Test tenant creation, membership creation, and cleanup as an atomic or recoverable workflow; include the failure where the tenant exists but membership does not.
  7. Review server-side handlers separately because Admin SDK access bypasses client Security Rules.

A useful first implementation is intentionally boring: resolve one tenant, verify one membership document, and read one tenant path. Once that is correct and covered by emulator tests, you can add invites, seat limits, billing ownership, and per-tenant permissions without first untangling global data.

Sources

Your next step

Put this pattern into a working SaaS foundation

Start with connected authentication, billing, dashboards, and provider boundaries—then spend your build time on what makes your product different.

AI-friendly architecture
Production ready from day one
Lifetime updates