# Custom Roles Settings

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 163
**Tier:** All tiers
**Depends on:** `foundation-auth-rbac`, `team-users-settings`
**Referenced by:** `foundation-auth-rbac`, `staff-portal-detail`

---

## Overview

Spec 3 (`foundation-auth-rbac`) defines the permission matrix and mentions "Tenant admins can create custom roles at `/settings/roles`." Spec 138 (`team-users-settings`) covers member invite and role assignment. Neither defines the `/settings/roles` page where custom roles are created and managed.

This spec defines the UI and API for custom role management: create, edit, delete, and assign permissions to custom roles.

---

## Route

`/settings/roles` — requires `users:manage`.

---

## Page Layout

```
┌──────────────────────────────────────────────────────────────┐
│  Settings > Roles                        [+ New role]        │
│                                                              │
│  ┌── System roles (read-only) ─────────────────────────────┐ │
│  │  OWNER   — full access                                  │ │
│  │  ADMIN   — manage team + settings, not billing          │ │
│  │  MEMBER  — standard access                              │ │
│  │  CONTRACTOR — time + projects access (limited)          │ │
│  └─────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌── Custom roles ─────────────────────────────────────────┐ │
│  │  Finance Manager   4 members   [Edit]  [Delete]         │ │
│  │  Support Lead      2 members   [Edit]  [Delete]         │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```

System roles listed as reference only — no edit/delete controls.

---

## Create / Edit Role Modal

```
┌─────────────────────────────────────────────────────────────┐
│  New role                                           [✕]     │
│                                                             │
│  Role name                                                  │
│  [Finance Manager_______]                                   │
│                                                             │
│  Permissions                                                │
│  ┌── Invoices ──────────────────────────────────────────┐  │
│  │  ☑ invoices:read   ☑ invoices:write   ☐ invoices:delete  │
│  │  ☐ invoices:send                                      │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Billing ────────────────────────────────────────────┐  │
│  │  ☑ billing:read    ☐ billing:manage                   │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Customers ──────────────────────────────────────────┐  │
│  │  ☑ customers:read  ☐ customers:write  ☐ customers:delete │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Tasks ──────────────────────────────────────────────┐  │
│  │  ☑ tasks:read  ☐ tasks:write  ☐ tasks:assign  ☐ tasks:delete │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Projects ───────────────────────────────────────────┐  │
│  │  ☑ projects:read  ☐ projects:write  ☐ projects:delete │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Expenses ───────────────────────────────────────────┐  │
│  │  ☑ expenses:read  ☐ expenses:write                    │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Time ────────────────────────────────────────────────┐  │
│  │  ☑ time:read  ☐ time:track  ☐ time:manage             │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Marketing ──────────────────────────────────────────┐  │
│  │  ☑ marketing:read  ☐ marketing:write                  │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Reports ─────────────────────────────────────────────┐  │
│  │  ☑ reports:read  ☐ reports:export                     │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── KB ──────────────────────────────────────────────────┐  │
│  │  ☑ kb:read  ☐ kb:write  ☐ kb:delete  ☐ kb:share       │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Tickets ─────────────────────────────────────────────┐  │
│  │  ☑ tickets:read  ☐ tickets:write  ☐ tickets:assign     │  │
│  │  ☐ tickets:resolve                                     │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Calendar ─────────────────────────────────────────────┐  │
│  │  ☑ calendar:read  ☐ calendar:write                    │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌── Settings & Users ────────────────────────────────────┐  │
│  │  ☐ users:read  ☐ users:invite  ☐ users:manage  ☐ users:freeze │
│  │  ☐ settings:read  ☐ settings:write                     │  │
│  │  ☐ webhooks:read  ☐ webhooks:manage                    │  │
│  │  ☐ payouts:read  ☐ payouts:manage                      │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  [Cancel]                               [Save role]        │
└─────────────────────────────────────────────────────────────┘
```

Permission groups mirror the categories from spec 3. All permissions shown even if the tenant doesn't use that feature area.

---

## Delete Role

Delete button on custom role row:
- If the role has 0 members: confirm dialog → delete immediately.
- If the role has ≥ 1 member: confirm dialog shows member count and requires selecting a replacement role:

```
Delete "Finance Manager"?
This role is assigned to 4 members.
Reassign members to: [MEMBER ▾]
[Cancel]  [Delete and reassign]
```

On confirm: all members with deleted role reassigned to selected replacement, then role deleted. Atomic in DB transaction.

---

## API

```
GET  /api/roles
     → list custom roles for tenant
       Returns: [{ id, name, permissions: string[], member_count }]
       Requires: users:manage

POST /api/roles
     → create custom role
       body: { name: string, permissions: string[] }
       Requires: users:manage
       Validation: name non-empty, unique per tenant; permissions must be valid strings from the permission vocabulary

PATCH /api/roles/:id
      → update custom role (name or permissions)
        body: { name?: string, permissions?: string[] }
        Requires: users:manage

DELETE /api/roles/:id
       → delete custom role
         body: { reassign_to_role_id: string } — required if member_count > 0
         Requires: users:manage
```

---

## Schema Delta

```sql
CREATE TABLE IF NOT EXISTS tenant_roles (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id   UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name        TEXT NOT NULL,
  permissions TEXT[] NOT NULL DEFAULT '{}',
  created_at  TIMESTAMPTZ DEFAULT now(),
  UNIQUE (tenant_id, name)
);
```

`tenant_memberships.role` (TEXT) continues to reference system role names (`OWNER`, `ADMIN`, `MEMBER`, `CONTRACTOR`) or `tenant_roles.id::text` for custom roles (existing pattern from spec 3).

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `permissions TEXT[]` | Not FK to a permissions table | Permission strings are code-defined constants; a permissions table would need migrations every time a new scope is added; TEXT[] is flexible and easy to query with `@>` |
| Atomic reassign + delete | Single transaction | Leaving members without a role on delete would break auth middleware; transaction ensures atomic transition |
| All permissions shown in UI | Not filtered by tenant's active features | Simpler — no "which features does this tenant use?" logic in the role editor; unchecked permissions are harmless if the feature is unused |
| System roles not editable | Not fully custom | System roles (`OWNER`, `ADMIN`, `MEMBER`, `CONTRACTOR`) have hardcoded permission sets in `packages/auth`; editable system roles would require special handling or code divergence |
