# Support access

Source: https://permdock.com/docs/guides/support-access

Let staff act inside a customer's account with better-supabase support sessions and a PermDock delegation, read-only by default, enforced in process and in Postgres, and attributed on every decision.

A support session lets a staff member see what a customer sees, without the customer's password and without becoming the customer. With better-supabase, the token belongs to the user and names the staff member in `act`; PermDock reads that as an actor and lets it reach only what a policy delegation names. A session is read-only unless better-supabase started it with `read_only: false`.

`apps/examples/next-better-supabase` runs every step on this page: `tests/support-access.test.ts` for the in-process decisions and `supabase/tests/020_support_read_only.test.sql` for Postgres.

## Pick the shape [#pick-the-shape]

| Shape | Principal | Actor | Use it when |
| --- | --- | --- | --- |
| better-supabase support session | The user | The admin | Staff reproduce what one user sees, inside that user's grants |
| `supportAccess` with consent | The vendor | The engineer | A vendor team works in a tenant under a consented, expiring grant |

The first is this guide. The second is [support access with tenant consent](/docs/concepts/elevated-access#support-access-with-tenant-consent): the vendor holds its own `via: 'support'` membership, so it never reaches more than the tenant consented to, whoever the user is. Impersonation in the narrow sense (`act.kind: 'impersonation'`) is a third token kind; it reaches nothing until a delegation names `actor('impersonation')`, and this guide never adds one.

## Delegate to the support actor [#delegate-to-the-support-actor]

A support token carries no delegation of its own, so every decision denies with `no-delegation` until the policy names the actor kind ([policy delegations](/docs/security/delegation#policy-delegations)):

```ts
// src/policy.ts
export const policy = definePolicy(permissions, {
  roles: [owner, member, contact],
  delegations: [
    {
      from: roles.owner,
      to: actor("support"),
      permissions: [permissions.quotes],
    },
  ],
});
```

The session reaches the intersection of three sets: what the user holds, what the delegation names, and, for a read-only session, the permissions whose `readOnlyHint` is true (`meta.readOnly`, else a `read` or `list` action).

| Session | `quotes.read` | `quotes.update` | `staff.list` |
| --- | --- | --- | --- |
| The owner's own | granted | granted | granted |
| Support, `read_only: true` | granted | denied, `not-delegated` | denied |
| Support, no `read_only` claim | granted | denied, `not-delegated` | denied |
| Support, `read_only: false` | granted | granted | denied |
| Support without `session_id` | denied | denied | denied |
| `act.kind: 'impersonation'` | denied, `no-delegation` | denied, `no-delegation` | denied |

A support level without `read_only` is read-only, as better-supabase reads it. A support level without a `session_id`, or with a `read_only` that is not a boolean, is not proof of who acts: `subjectFromSupabase` returns the anonymous subject ([actors](/docs/adapters/supabase#support-and-impersonation-actors)).

## Enforce it in Postgres [#enforce-it-in-postgres]

PermDock's generated RLS helpers read memberships, not `act`. A support token sent straight to the Data API is the user to Postgres and writes whatever the user may write. Set `rls.readOnlyActors` so `permdock rls generate` adds one restrictive policy per table and write command it grants, and the database applies the same read-only default:

```ts title="permdock.config.ts"
rls: {
  dialect: "supabase",
  readOnlyActors: true, // support and impersonation sessions
},
```

Each policy (`quotes_update_read_only_actors`, `quotes_insert_read_only_actors`, ...) refuses the write when `act.kind` is `support` or `impersonation`, or when `act` carries a `session_id` and no `kind` (better-supabase 0.5.0 tokens), unless `act.read_only` is `false`. A restrictive policy is combined with `and`, so it only ever removes rows the permissive policies allow, and reads are untouched. The policies do not narrow a session to the delegation's permissions; Postgres sees only the user's grants, so keep writable tables behind the server where the delegation applies, or add the delegation's limits to the policy. [Read-only actors](/docs/cli/rls#read-only-actors) has the generated SQL and the options. A table whose policies are hand-written (`--helpers-only`) needs the same restrictive policy written by hand.

## Audit [#audit]

Every decision event carries both identities: `subject.principal` is the user and `subject.actor` is `{ id, kind: 'support' }`. Send them to a [decision sink](/docs/concepts/audit-and-observability) and a reviewer can list what a staff member did in each session. better-supabase's audit log fills `impersonated_by` (the `act.sub`) and `support_session_id` (the `act.session_id`) on every write the session makes. The decision event carries the actor id but not the session id, so join the two logs on the actor id and time.

PermDock keeps no support-session state. A session ends when its token stops verifying, which is decided upstream ([authentication](/docs/concepts/authentication)).

## Test it [#test-it]

```ts
const permdock = await createPermDock(
  policy,
  subjectFromSupabase(supportClaims),
  {
    tenant: organization,
  },
);
expect(permdock.decide(permissions.quotes.update, quote)).toMatchObject({
  outcome: "denied",
  denials: [{ reason: "not-delegated" }],
});
```

`supabaseClaimFixtures` in `permdock/testing` has `supportSession`, `supportSessionReadOnly` and `impersonation` tokens for the same cases ([Supabase claims schema](/docs/adapters/supabase)).
