Series · Part 3 of 6
Building Djengo
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#
| Field | Level | Role |
|---|---|---|
| organization_id | Group | Top-level billing and multi-company root |
| company_id | Operating company | The primary data isolation key |
| branch_id | Property or site | Narrows ops to one hotel, campus, or estate phase |
-- every tenant-owned row
organization_id UUID NOT NULL,
company_id UUID NOT NULL, -- when company-scoped
branch_id UUID NULL -- when property-scopedEnforced in layers#
- 1Gateway: an org auth guard reads the session and writes x-organization-id, x-company-id, and x-branch-id into gRPC metadata.
- 2NestJS services: a Prisma extension backed by async-local storage adds the company and organization filter to every query automatically.
- 3C# services: a shared TenantScope helper asserts the ids on every read and write.
- 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.
- 1Resolve organization and company from the token.
- 2Resolve the active property from the session or route.
- 3Keep role assignments that are company-wide or match that branch.
- 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.
← Part 2
Inside Djengo: one gateway, ten services, two languages
Part 4 →
One facility model for hotels, hospitals, and estates
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.





