---
title: "Zero-Trust Multi-Tenant Firestore: Middleware, Claims, and 60+ Wildcard Permissions"
canonical_url: "https://cognilium.ai/blogs/zero-trust-multi-tenant-firestore"
slug: "zero-trust-multi-tenant-firestore"
section: "blogs"
date_published: "2026-05-05"
date_modified: "2026-05-05"
word_count: 1700
reading_time_minutes: 9
author: "Mudassir Marwat"
author_identifier: "0009-0008-1927-2598"
person_same_as:
  - "https://www.linkedin.com/in/mudassir-marwat/"
  - "https://orcid.org/0009-0008-1927-2598"
  - "https://github.com/mudassirmarwat"
  - "https://medium.com/@mudassir-marwat"
  - "https://www.youtube.com/@mudassir-marwat"
  - "https://dev.to/mudassirmarwat"
  - "https://hashnode.com/@mudassirmarwat"
  - "https://huggingface.co/mudassirmarwat"
  - "https://bsky.app/profile/mudassir-marwat.bsky.social"
  - "https://substack.com/@mudassirmarwat"
  - "https://topmate.io/mudassirmarwat"
  - "https://fueler.io/mudassirmarwat"
  - "https://www.instagram.com/mudassirmarwat/"
  - "https://www.facebook.com/mudassir.marwat"
  - "https://www.reddit.com/user/mudassirmarwat/"
  - "https://www.f6s.com/member/mudassir-marwat"
  - "https://cognilium.ai/founder"
entities: []
related:
  - "https://cognilium.ai/blogs/8-stage-docint-pipeline"
  - "https://cognilium.ai/blogs/anti-hallucination-domain-vocabulary-grounding"
  - "https://cognilium.ai/blogs/organizational-memory-rag-slack-confluence-loom"
---
# Zero-Trust Multi-Tenant Firestore: Middleware, Claims, and 60+ Wildcard Permissions

Hard tenant isolation on Firestore: middleware, immutable claims, wildcard permissions. The architecture that makes leakage structurally impossible.

## Key takeaways

- Tenant isolation is strongest when it is structural — enforced on the query path, not by remembering to filter.
- Middleware establishes tenant context once; immutable claims stop it being reassigned further down the stack.
- Wildcard-driven permissions keep the rule set small enough to reason about as it grows.
- Secrets stay backend-only and are never serialised into a response.

Hard multi-tenancy on Firestore — where tenant A literally cannot see tenant B's data — is not a query-pattern. You cannot "remember to filter by org_id" your way to security. It has to be a middleware layer, an immutable claim source, and a permission model that expands cleanly without granting more than intended. The architecture below is what shipped.

## TenantContextMiddleware

Every API request hits a middleware that does three things: reads the immutable Firebase custom claim, looks up organizations/{orgId}/permissions in Firestore, and attaches a request-scoped context to the request object. Every downstream operation reads from request.context — never from the raw token, never from the URL, never from a header the client controls.

- Custom claim: set at signup by an admin SDK action; immutable until rotated
- Permissions doc: editable by org owners; read-only at request time
- Context: {orgId, userId, roles[], permissions[]} — all derived, none client-supplied

## Firestore queries: scoped at the path layer

Every collection lives under organizations/{orgId}/. There is no top-level documents collection — only organizations/{orgId}/documents. A query that omits the orgId path segment fails at dispatch. The system has no way to "accidentally" query across tenants because the path itself enforces scope.

## Permissions: 60+ wildcard-driven

Permission strings: "{resource}:{action}:{scope}". Examples: "documents:read:org" (read documents in own org), "documents:*:org" (any action on documents), "billing:read:platform" (read billing across platform — admin-only). 5 platform/org roles map to permission sets; assignment is per-user.

- Wildcard expansion at permission-check time, not at storage. "documents:*:org" expands to read+write+delete+list when checked.
- Scope is the third segment — "org" stays in the user's tenant; "platform" cross-tenant; "self" only the user's own resources.
- 60+ raw permissions; 5 roles bundle them; assignment is per-user with override support.

## Secrets: backend-only, never serialized

Tenant secrets (API keys, tokens, OAuth refresh tokens) live in a backend-only collection. Firestore security rules deny all client reads. Only the backend service account can read them, only at the moment of use. Secrets never travel to the frontend, never appear in any response payload, never get logged.

## What can still go wrong

- A developer forgets the middleware on a new endpoint — the query fails because of path scoping, but error messages might leak existence info. Mitigation: 401 on auth failure, generic 404 on missing resource, never 403.
- A client manipulates a URL parameter to access another org's resource — middleware reads the immutable claim, ignores the URL, scopes the query correctly.
- A wildcard expansion grants more than intended — quarterly permission audit + a test suite that asserts no wildcard expands to a cross-scope grant.
- Custom claim rotation gap — there is a 1-hour window where the old claim is still valid. Mitigation: rotate-and-revoke is a two-step process documented in runbooks.

## Numbers from production

- 60+ permissions, 5 roles, 15 Firestore composite indexes
- 7 alert policies (latency, error rate, auth failures, permission denials, cross-tenant attempts)
- 8 Cloud Scheduler jobs for periodic audits and maintenance
- P95 endpoint latency: 5s alert; permission-check latency: 5-10ms (cached)

## What this architecture is and is not

It is hard tenant isolation, defense in depth, and a permission model that survives growth. It is not a complete zero-trust architecture (which also requires mTLS between services, secrets management beyond Firestore, audit logging, and incident-response runbooks). The architecture above is the part that lives in the application code; the rest belongs in infrastructure.

## Frequently asked questions

### Why custom claims instead of a tenants table?

Claims are immutable per-token, set at signup, and travel with every request automatically. Looking up a tenants table on every request adds Firestore read latency. Claims also cannot be tampered with by the client — they are signed.

### What does "wildcard permission" mean?

A permission scope like "documents:*" granting all actions on documents, vs. "documents:read." Wildcards are convenient for admin roles; the system validates wildcard expansion at permission-check time so a "documents:*" never accidentally grants "billing:read."

### Why 60+ permissions? That sounds like a lot.

Per-resource × per-action × per-scope. ~10 resources × 4 actions × scopes = 60+. The number itself is fine — what matters is permission cardinality stays manageable. Wildcards keep roles short.

### How is the middleware bypassed for admin operations?

It is not. Even cross-tenant admin operations go through TenantContextMiddleware with a special "platform" claim that scopes operations to the platform namespace. There is no "skip middleware" path.

### What is the failure mode if a developer forgets the middleware?

A query without tenant context throws on dispatch — no fallback to "global." Forgetting is a build/test-time failure, not a runtime data-leak. Critical: the failure has to be early-and-loud, not silent.

---

Canonical HTML: https://cognilium.ai/blogs/zero-trust-multi-tenant-firestore
