> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Scoped Access

> Admin-managed form access grants for editors and viewers

# Scoped Access

> Scoped access lets admins assign specific forms to editors and viewers, restricting their dashboard to only the forms they need.

## Overview

Users with global roles `editor` or `viewer` don't automatically see all forms. Instead, an admin explicitly grants them access to individual forms through the **Manage Access** dialog on the admin settings page (`/settings/admins`).

```mermaid theme={null}
flowchart LR
    Admin[Super Admin] -->|grants access| UFA[user_form_access]
    UFA -->|editor can edit| F1[Form A]
    UFA -->|editor can edit| F2[Form B]
    UFA -.->|no access| F3[Form C]

    style F3 stroke-dasharray: 5 5
```

## How It Works

### Two Access Systems

Terra has two independent systems for form-level access:

| System            | Table              | Purpose                    | Managed By                    |
| ----------------- | ------------------ | -------------------------- | ----------------------------- |
| **Scoped Access** | `user_form_access` | Admin-assigned form grants | Super admins via Settings     |
| **Team Members**  | `program_members`  | Per-form team invitations  | Form owners via Form Settings |

Both systems are read at login time and merged — the higher role wins per form.

### Role Resolution

When determining a user's effective role for a form, the system:

1. Queries `user_form_access` by `user_profile_id` (admin grants)
2. Queries `program_members` by `user_id` and `email` (team invitations)
3. Merges results — higher role wins (`owner > editor > viewer`)
4. Applies global role overrides (global `viewer` caps to read-only)

```typescript theme={null}
// lib/form-scope.ts — simplified
const combined = [
  ...programMemberRows,   // legacy team grants
  ...userFormAccessRows,   // admin-managed grants
];

for (const row of combined) {
  const effective = applyGlobalRoleOverride(row.role, globalRole);
  rolesByFormId[row.form_id] = pickHigherRole(rolesByFormId[row.form_id], effective);
}
```

### Global Role Overrides

Global roles set a ceiling on form-level permissions:

| Global Role             | Effect                                                     |
| ----------------------- | ---------------------------------------------------------- |
| `super_admin` / `admin` | Full access to all forms (bypass scoped access)            |
| `editor`                | Can edit assigned forms only                               |
| `viewer`                | Read-only on assigned forms, even if form role is `editor` |
| `user`                  | Standard form-level permissions apply                      |

## Database Schema

### user\_form\_access

The dedicated table for admin-managed grants, introduced in migration `093`.

```sql theme={null}
CREATE TABLE user_form_access (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_profile_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
  form_id UUID NOT NULL REFERENCES forms(id) ON DELETE CASCADE,
  role TEXT NOT NULL DEFAULT 'viewer',
  granted_by TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_profile_id, form_id)
);
```

**Design decisions:**

* References `user_profiles(id)` — a stable UUID PK — instead of `auth.users`, avoiding FK issues with WorkOS user IDs
* Uses `TEXT` for `role` instead of an enum, avoiding `program_role` type compatibility issues across deployments
* `granted_by` stores the admin's user ID for audit purposes

## Server Actions

### getUserScopedAccess

Returns the list of available forms and the user's current assignments.

```typescript theme={null}
// app/actions/system.ts
const result = await getUserScopedAccess(userId);
// → { forms, workspaces, selectedFormIds }
```

### updateUserScopedAccess

Replaces all form assignments for a user. Uses delete-then-insert for clean state:

```typescript theme={null}
// app/actions/system.ts
await updateUserScopedAccess(userId, ["form-id-1", "form-id-2"]);
```

Internally:

1. Looks up `user_profiles.id` by `user_id`
2. Deletes all existing `user_form_access` rows for that profile
3. Inserts new rows for each selected form

## UI

The **Manage Access** dialog is available on `/settings/admins` for users with `editor` or `viewer` roles. Admins can:

* See all available forms grouped by workspace
* Toggle individual forms on/off
* Save assignments with a single click

Only `super_admin` and `admin` users can manage scoped access for others.

***

<CardGroup cols={2}>
  <Card title="Authorization" icon="shield" href="/core-systems/identity/authorization">
    Global role hierarchy
  </Card>

  <Card title="Multi-Tenancy" icon="building" href="/core-systems/identity/multi-tenancy">
    Workspaces and folders
  </Card>
</CardGroup>
