> SUPERSEDED 2026-05-26 — Single-tenant BMW conversion (refactor/remove-control-plane). Historical record only.

# ADR 003: V1 — Dedicated Supabase Project Per Tenant

## Status
Accepted (V1 only)

## Date
2026-04-05

## Context

Each tenant company requires isolated data storage. Two multi-tenancy models were considered:

**Option A: Shared Supabase project with `tenant_id` columns**
All tenants live in the same database. Isolation enforced by RLS policies checking `tenant_id = auth.jwt() ->> 'tenant_id'`. Tenant ID embedded in JWT claims.

**Option B: Dedicated Supabase project per tenant**
Each tenant company gets their own Supabase project: separate database, auth service, storage bucket, and API keys. Tenant identity comes from environment variables, not JWT claims.

## Decision

**Option B — dedicated Supabase project per tenant** for V1.

## Rationale

1. **Physical isolation by default**: A bug in RLS policies cannot leak data between tenants. The connection string itself is the boundary. This is the highest isolation guarantee possible.

2. **No RLS policy complexity for tenant data**: Existing tables do not need `tenant_id` columns. RLS policies remain focused on user roles within the tenant, not cross-tenant separation. Simpler, less error-prone.

3. **Independent scaling and backups**: Each tenant's Supabase project can be scaled, backed up, or migrated independently. A high-traffic tenant doesn't affect others.

4. **BMW supplier context**: Customers are manufacturing companies with strict data sensitivity requirements. Physical isolation is the correct default for enterprise B2B.

5. **Simple mental model**: Developers always work against "the current tenant's database." No `tenant_id` filter needed on any query. Supabase client connects to the right project by environment variable.

6. **V1 scale fits**: At <50 tenants, dedicated projects are operationally manageable and cost-acceptable.

## Consequences

- Each tenant deployment has unique env vars: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `TENANT_ID`.
- Provisioning must create a new Supabase project (or configure a dedicated schema) for each tenant.
- Schema migrations must be applied to each tenant project separately during provisioning.
- The control plane tracks each tenant's environment config in `cp_tenant_environments`.

## Migration Path to Shared Multi-Tenant (Post-V1)

When scale exceeds 50 tenants:

1. Add `tenant_id UUID NOT NULL` to all tenant tables.
2. Update RLS policies: `USING (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid)`.
3. Configure Supabase Auth to embed `tenant_id` in JWT claims via custom claims hook.
4. Update `TENANT_ID` env var usage: read from JWT instead of environment.
5. Entitlement and branding engines already accept `tenantId` parameter — no change needed.
6. Merge all per-tenant schemas into one shared project with a data migration script.

This migration is safe because:
- All queries go through Supabase client (no raw cross-tenant SQL).
- `TENANT_ID` is accessed only through `getTenantId()` helper — one change point.
- Feature entitlement checks already pass `tenantId` explicitly.

## Rejected Alternative

**Option A** was not chosen for V1 because: requires JWT claims infrastructure upfront, adds `tenant_id` to every table and every query, RLS policy mistakes can leak cross-tenant data, more complex provisioning. Correct choice at scale, not at launch.
