ADR-001: Multi-Tenancy and Access Model
Status: Accepted Owner: @bilal @deen Date: 2025-12-06
Context
Envo serves multiple property management organisations. We need to:
- Isolate data between organisations completely
- Allow multiple users to access a single organisation
- Allow a single user to access multiple organisations
- Support Envo staff temporarily accessing any organisation for support
- Maintain full audit trails of who accessed what
Options Considered
| Option | Pros | Cons |
|---|---|---|
| A: Schema-per-tenant | Complete isolation, simpler RLS | Complex migrations, connection pooling issues, harder cross-tenant queries |
B: Single DB with landlord_id | Simple, standard | Terminology locked, doesn’t model orgs cleanly |
C: Single DB with organisation_id + junction table | Flexible, supports all access patterns, future-proof | Slightly more complex RLS |
Decision
Option C: Single database with organisations and junction-based access.
Data Model
users (synced from Supabase Auth)
├── id (uuid, maps to auth.uid())
├── email, full_name, phone
├── created_at, updated_at
organisations
├── id (uuid)
├── name, type (company | individual | hmo_operator)
├── plan (basic | premium | partner)
├── billing_email
├── created_at, updated_at, deleted_at
user_organisation_access
├── user_id (FK → users)
├── organisation_id (FK → organisations)
├── role (owner | admin | staff | envo_support)
├── granted_by, granted_at, expires_at, revoked_at, is_active
Access Patterns
- Organisation owner:
role = 'owner',expires_at = null - Organisation staff:
role = 'staff',expires_at = null - Envo support access:
role = 'envo_support',expires_at = NOW() + interval '4 hours'
Session Scoping
- Query
user_organisation_accessfor active, non-expired access on login - If multiple organisations, present picker
- Store selected
organisation_idin session/JWT claims - All RLS policies filter by
organisation_idfrom session
Consequences
Positive
- Clean separation of identity (users) from tenancy (organisations)
- Supports all current and future access patterns
- Full audit trail via
granted_by,granted_at,revoked_at - Envo staff access is time-boxed and auditable
Negative
- RLS policies slightly more complex (junction table join)
- Session must carry
organisation_idcontext - Migration needed if schema-per-tenant required for compliance later
Risks
- Performance: Junction table adds a join. Mitigated by indexing + caching in JWT.
- Complexity: More tables. Mitigated by clear naming and documentation.