# Pattern 11 — Admin cross-tenant access

## What this is

A pattern for giving internal admins visibility across all tenants while regular users stay isolated to their own org. Uses a custom JWT claim in `app_metadata` to distinguish admin from regular authenticated users.

## Two policies, not one

The clean approach keeps the admin path and the tenant path in separate policies:

```sql
-- Regular users see their org
create policy "tenant_select_user" ...
  using (org_id = (select org_id from users where id = auth.uid()));

-- Admins see everything
create policy "tenant_select_admin" ...
  using ((auth.jwt() -> 'app_metadata' ->> 'app_role') = 'admin');
```

Because both policies are `permissive` (the default), PostgreSQL ORs them — a user satisfying either policy gets access. Admins satisfy the admin policy and see all rows; regular users satisfy the tenant policy and see only their org's rows.

## Why app_metadata, not user_metadata

Supabase Auth has two metadata fields:

| Field | Who can write | Use for |
|---|---|---|
| `user_metadata` | The user themselves (client-side) | Preferences, display name |
| `app_metadata` | Service role key only (server-side) | Role, org, privilege claims |

If you gate admin access on `user_metadata`, any user can promote themselves to admin by updating their own profile. Only `app_metadata` is safe for privilege claims.

```ts
// Correct: requires service_role key
await supabaseAdmin.auth.admin.updateUserById(userId, {
  app_metadata: { app_role: 'admin' }
});
```

## Anti-pattern: mixing admin and user logic in one policy

```sql
-- Avoid this — harder to audit and disable
using (
  org_id = (select org_id from users where id = auth.uid())
  or (auth.jwt() -> 'app_metadata' ->> 'app_role') = 'admin'
)
```

Separate policies make it easy to disable the admin path for audit, rotate admin credentials, or add admin-specific logging without touching user isolation logic.

## Audit requirement

Admin cross-tenant visibility creates compliance risk without logging. If an admin reads another tenant's data, that should be on record. Implement an audit log and populate it from your application layer (not RLS, which can't trigger side effects).
