Securing a Multi-Tenant Platform: Where Auth Meets Data Isolation
Enforcing tenant boundaries at every layer — JWT with tenant claims, middleware validation, audit logging, and defense in depth.
Context
Our multi-tenant platform had row-level isolation in the database, but we needed to ensure auth and API access enforced tenant boundaries at every layer. A bug in one place — a missing tenant check, an error message that leaked data — could expose one client's data to another. We needed defense in depth: validate tenant context on every request, audit cross-tenant access attempts, and never trust the client to pass the correct tenant.
Constraints
- JWT or session must carry tenant context — but we couldn't trust the client to set it correctly
- Error messages and logs must not leak cross-tenant data
- We needed an audit trail for compliance — who accessed what, when
Architecture
We issue JWTs with tenant_id in the payload, but we derive the tenant from the authenticated user's membership — never from a client-supplied header. Middleware on every API route validates that the request's tenant matches the user's tenant; a mismatch returns 403. We added audit logging for sensitive operations: who, what, when, and which tenant. Error messages are generic ('Resource not found') rather than revealing whether a resource exists in another tenant. We considered RLS as a backstop but kept application-level enforcement as the primary control — RLS would have been a safety net, but we wanted the main logic explicit in code.
Alternatives considered
- Separate auth service per tenant: Doesn't scale. We'd have N auth deployments for N tenants; shared auth with tenant claims is simpler.
- Trust the frontend to pass tenant context in headers: Never trust the client. A malicious or buggy client could send any tenant_id; we derive it from the authenticated user.
Lessons learned
- Derive tenant from identity, never from request params. The user's tenant is a fact of their account, not something they tell us.
- Audit logging is cheap to add and expensive to retrofit. We log at the service layer so we don't miss anything.
- Generic error messages protect against enumeration. 'Not found' is safer than 'not found in your tenant.'