← All articles
Security·3 min read

Multi-tenancy without a tenant_id

Djengo isolates data with a composite scope of organization, company, and branch. How that scope is carried, enforced in layers, and tested with a demo tenant.

djengoarchitecturesecuritysaasbackend
On this page

Most SaaS tutorials give you one column: tenant_id. It works until your first customer is a group that owns three hotels under two companies and wants one manager to run only the Ikeja property. Djengo does not have a tenant_id. It has a scope.

Three levels, one scope#

FieldLevelRole
organization_idGroupTop-level billing and multi-company root
company_idOperating companyThe primary data isolation key
branch_idProperty or siteNarrows ops to one hotel, campus, or estate phase
sql
-- every tenant-owned row
organization_id  UUID NOT NULL,
company_id       UUID NOT NULL,  -- when company-scoped
branch_id        UUID NULL       -- when property-scoped

Enforced in layers#

  1. 1Gateway: an org auth guard reads the session and writes x-organization-id, x-company-id, and x-branch-id into gRPC metadata.
  2. 2NestJS services: a Prisma extension backed by async-local storage adds the company and organization filter to every query automatically.
  3. 3C# services: a shared TenantScope helper asserts the ids on every read and write.
  4. 4Next: PostgreSQL row-level security keyed on company_id, so a missed filter fails closed.

No single layer is trusted to be perfect. The gateway can be wrong, a service can forget, an ORM extension can be bypassed with raw SQL. Defense in depth is not paranoia when the data is payroll.

Roles that know which property#

A role assignment can carry an optional branch. Property scope is additive to company scope, never a replacement. The same person can manage Cascade Ikeja and be ordinary staff at Cascade VI.

  1. 1Resolve organization and company from the token.
  2. 2Resolve the active property from the session or route.
  3. 3Keep role assignments that are company-wide or match that branch.
  4. 4Merge the permissions from those roles.

A demo tenant is a tenancy test#

On staging and previews, the staff app has a Demo | Live switch. In Demo mode you stay signed in as yourself, but the tenant headers are forced to seeded demo ids: Harbour Inn with one branch, Cascade Residences with three. If any screen shows your real data in Demo mode, scope leaked. Production never shows the switch.

Checklist for every new module

  • Table has organization_id and company_id, plus branch_id if it is property-specific.
  • Indexes on (company_id, status) and real foreign keys.
  • List and get endpoints take company from auth context, not the client.
  • Create validates that body ids match the caller's scope.
  • HR and financial records soft delete with deleted_at.

Key takeaways

What to remember

  • Scope is organization, company, and branch, not one id.
  • Take scope from auth context, never the body alone.
  • Enforce in layers so one miss fails closed.
Isaac Tubonibo

Written by

Isaac Tubonibo

Founder of Djengo · Software Engineer

Founder of Djengo, operations software for hotels, hospitals, estates, and restaurants. I build it end to end: architecture, delivery, and the human side of shipping under pressure.

More from the archive

Related articles