Multi-Tenant Gym Schema & Row-Level Isolation
Multi-Tenant Gym Schema & Row-Level Isolation
Released in v1.0.1
This release lays the data foundation for the platform's multi-tenant architecture. It introduces the core Neon Postgres schema for gyms, coaches, and members, and enforces tenant isolation using Postgres Row-Level Security (RLS) policies.
Why Multi-Tenancy Matters
The platform is a B2B SaaS product — each gym is an independent tenant. This means:
- Gym A must never see Gym B's members, sessions, or business data.
- A coach at one gym must not be able to query records belonging to another.
- Platform administrators need visibility across all tenants for support and analytics.
Rather than relying solely on application-level filtering, isolation is enforced at the database layer using Postgres RLS, providing a strong security guarantee regardless of how queries are constructed.
Core Schema Entities
| Entity | Description |
|---|---|
gyms | The top-level tenant record. Every other entity belongs to a gym. |
coaches | Staff accounts associated with a gym. Can author and deliver sessions. |
members | End-user athlete accounts registered under a gym. |
All tables carry a tenant identifier that binds records to a specific gym. This identifier is set at row-creation time and is immutable.
Row-Level Security Policies
How It Works
Postgres RLS allows policies to be attached directly to tables. When a query is executed, the database engine evaluates the active policy and transparently filters or blocks rows that do not belong to the current tenant context.
The platform sets the tenant context on the database connection at the start of each authenticated request. All subsequent queries in that session automatically respect the RLS policies — no application-level WHERE gym_id = ? clauses are required (though they may still be used for clarity).
Policy Model
┌─────────────────────────────────────────────┐
│ Incoming Request │
│ (authenticated as Gym X) │
└────────────────────┬────────────────────────┘
│
▼
Set tenant context on
Postgres connection
│
▼
┌─────────────────────────────────────────────┐
│ Postgres RLS Engine │
│ Applies tenant-scoped policy: │
│ WHERE gym_id = current_tenant_id() │
└────────────────────┬────────────────────────┘
│
▼
Only Gym X rows are returned
Cross-gym rows are invisible
Platform-Owner Bypass
Platform administrators operate under a privileged role that is exempt from tenant-scoped RLS policies. This allows:
- Cross-tenant queries for customer support.
- Aggregated analytics across all gyms.
- Administrative operations such as migrations and audits.
This bypass is role-based and is not available to any gym-level user, coach, or member.
Security Guarantees
| Scenario | Outcome |
|---|---|
| Gym A queries member records | Returns only Gym A's members |
| Gym A attempts to access Gym B's data | Zero rows returned — transparent isolation |
Application bug omits a gym_id filter | RLS policy still enforces isolation |
| Platform admin queries all members | Full cross-tenant result set returned |
Defence in depth: RLS acts as a second layer of isolation. Even if an application-level access control check is misconfigured or missing, the database will not return records outside the authenticated tenant's scope.
Neon Postgres
The schema runs on Neon Postgres, a serverless Postgres platform. Neon's connection pooling and branch-based workflow are compatible with Postgres RLS — policies are evaluated at the Postgres engine level regardless of the connection infrastructure in front of it.
Related
- Changelog — Full version history