Multitenancy
Multi-tenant SaaS applications need authorization that respects organizational boundaries. Sentinel models tenants in role assignments and evaluation — not as an afterthought in middleware.
Tenant-scoped role assignments
const user: Subject<AppSchema> = {
id: "user-1",
roles: [
{ role: "admin", tenantId: "acme-corp" },
{ role: "viewer", tenantId: "globex" },
{ role: "member" }, // global
],
};Evaluation with tenantId
engine.evaluate(user, "invoice:approve", "invoice", {}, "acme-corp");
// Effective roles: admin, member (global)
engine.evaluate(user, "invoice:approve", "invoice", {}, "globex");
// Effective roles: viewer, member (global)Global roles (no tenantId on the assignment) apply in every tenant context. Use intentionally.
Where tenantId comes from
Typical sources:
- Subdomain (
acme.app.com) - Path prefix (
/orgs/acme/...) - Header (
X-Tenant-Id) — common in internal APIs - JWT claim resolved in auth middleware
Pass the same tenant identifier to evaluate() that you used when loading the user's memberships.
Strict tenancy
const engine = new AccessEngine<AppSchema>({
schema: {} as AppSchema,
strictTenancy: true,
});When enabled, if the subject has any tenant-scoped role assignment and you call evaluate() without tenantId, the engine throws.
Why: Forgetting tenant context should fail loudly in development, not evaluate with an ambiguous role set that might over- or under-permit.
Subjects with only global roles can still be evaluated without tenantId.
Resource context vs tenant
resourceContext.tenantId is optional metadata for conditions — it does not replace the evaluation tenantId parameter for role filtering.
Use the evaluation parameter for which hat the user wears. Use resource context when the rule depends on which organization owns the resource.
.when(ctx => ctx.resourceContext.tenantId === ctx.tenantId)Testing cross-tenant isolation
it("prevents cross-tenant admin powers", () => {
expect(engine.evaluate(user, "invoice:approve", "invoice", {}, "acme").allowed).toBe(true);
expect(engine.evaluate(user, "invoice:approve", "invoice", {}, "globex").allowed).toBe(false);
});Related pages
- Subjects and roles
- Express guide —
getTenantId