# Security

The CMS respects all four Quickback security layers. Every UI element — buttons, form fields, table columns, action menus — adapts based on the current user's role and the security rules defined in your feature files.

## CMS Access

`cms.access` decides whether the CMS is for platform sysadmins only (`"sysadmin"`, the default) or for organization members too (`"member"`). `user.role === "appmanager"` opens it in neither mode — that tier is the control plane, not the data plane.

```typescript
cms: { access: "member" }
```

The shell is served to any signed-in caller so the SPA can explain who is and isn't admitted; the gates that carry data are:

1. **Account UI** — the "Go to CMS" button is hidden for callers who wouldn't get in.
2. **`/api/v1/schema`** — the metadata endpoint returns `403` with `CMS_ACCESS_DENIED` and a hint naming the requirement. Without the registry there is no CMS.
4. **`custom_view` routes** — the CMS-internal saved-views table uses `userRole: ["appmanager"]` (and, when `cms.sysadmin: true`, also admits `sysadmin`), so saved views can't be read or modified from outside the CMS by non-operator accounts.
5. **In-SPA gate** — if any earlier layer is bypassed, the SPA still redirects the user to `/account/profile`.

Your resource API endpoints keep whatever `read`, `create`, `update`, `delete`, or `upsert` access rules you defined — they're the app's data API and unaffected by the CMS gate.

## The Four Layers

Quickback enforces security through four complementary layers:

| Layer | Purpose | CMS Behavior |
|-------|---------|-------------|
| **Firewall** | Tenant isolation (org-scoped data) | Handled server-side. CMS cannot bypass it. |
| **Operation Access** | Per-operation role requirements | Buttons hidden when role lacks permission |
| **Guards** | Field-level create/update control | Form fields enabled/disabled per guard rules |
| **Masking** | Sensitive field redaction | Masked values with lock icons per role |

## Role-Based Access

The CMS reads the user's role from their membership in the active organization. The three roles are:

- **owner** — Full access to all operations and data
- **admin** — Elevated access, typically all write operations and most actions
- **member** — Standard access with restrictions on sensitive data and destructive actions

### Operation Button Visibility

Create, edit, and delete buttons are shown or hidden based on the current role's access:

```typescript
read:   { access: { roles: ['member', 'admin', 'owner'] } },
create: { access: { roles: ['admin', 'owner'] } },
update: { access: { roles: ['admin', 'owner'] } },
delete: { access: { roles: ['owner'] } },
```

With the config above:

- **Members** see the table (read) but no Create, Edit, or Delete buttons
- **Admins** see Create and Edit buttons but no Delete
- **Owners** see all buttons

The row action menu also adapts — Edit and Delete entries only appear when the role has the corresponding permission.

## Guards

Guards control which fields appear in create and edit forms, and which cells are editable in Data Table mode.

### Createable Fields

Fields listed in `guards.createable` appear in the create form. Fields not in this list are hidden from the form entirely:

```typescript
guards: {
  createable: ['name', 'email', 'phone', 'status'],
}
```

### Updatable Fields

Fields listed in `guards.updatable` are editable in the edit form and in Data Table inline editing:

```typescript
guards: {
  updatable: ['name', 'email', 'phone'],
}
```

### Immutable Fields

Fields in `guards.immutable` can be set during creation but cannot be changed afterward. In edit mode, they appear as disabled inputs with a lock icon:

```typescript
guards: {
  immutable: ['accountCode', 'type'],
}
```

### Protected Fields

Fields in `guards.protected` can only be updated via specific actions. They appear as disabled inputs with the message "Updated via actions only":

```typescript
guards: {
  protected: {
    status: ['approve', 'void'],     // Updated by approve or void actions
    balance: ['applyPayment'],       // Updated by applyPayment action
  },
}
```

## Masking

Masking rules redact sensitive data based on the user's role. The CMS applies masking client-side for display and the API enforces it server-side in responses.

### Masking Types

| Type | Example Input | Masked Output |
|------|--------------|---------------|
| `email` | `john@acme.com` | `j***@acme.com` |
| `phone` | `(555) 123-4567` | `***-***-4567` |
| `ssn` | `123-45-6789` | `***-**-6789` |
| `redact` | `Confidential notes` | `------` |

### Configuration

```typescript
masking: {
  email: { type: 'email', show: { roles: ['admin', 'owner'] } },
  phone: { type: 'phone', show: { roles: ['admin', 'owner'] } },
  ssn:   { type: 'ssn',   show: { roles: ['owner'] } },
}
```

With the above config:

- **Members** see masked values for email, phone, and SSN
- **Admins** see unmasked email and phone, but masked SSN
- **Owners** see all values unmasked

### Visual Indicator

Masked fields display a lock icon next to the redacted value, making it clear that the field contains hidden data:

```
Email: [lock] j***@acme.com
SSN:   [lock] ***-**-6789
```

## Views

Views provide column-level projections per role. Only authorized views appear in the toolbar dropdown.

```typescript
read: {
  views: {
    summary: {
      fields: ['id', 'name', 'status'],
      access: { roles: ['member', 'admin', 'owner'] },
    },
    financial: {
      fields: ['id', 'name', 'balance', 'creditLimit', 'paymentTerms'],
      access: { roles: ['admin', 'owner'] },
    },
  },
}
```

Members only see the "summary" view option. Admins and owners see both "summary" and "financial". The "All Fields" option is always available.

## Firewall

The firewall layer handles tenant isolation — ensuring users can only access data within their organization. This is enforced entirely server-side:

- `organization_id` columns are filtered automatically by the API
- `owner` predicates restrict records to the creating user
- The CMS never sees data outside the user's organization scope

The CMS does not display firewall config in its UI because there is nothing for the user to control. Tenant isolation is transparent and automatic.

## Actions

Action visibility is controlled by access conditions on each action:

```typescript
actions: {
  approve: {
    access: {
      roles: ['admin', 'owner'],
      record: { status: { equals: 'pending' } },
    },
    cms: { destructive: true, confirm: true },
  },
}
```

The CMS evaluates both the role requirement and the record condition. In this example, the "Approve" action only appears for admins and owners, and only on records where `status === "pending"`.

Destructive actions (`cms.destructive: true`, or named `void`/`delete`) receive special warning styling with red text and an alert icon. Actions with `cms.hidden: true` are hidden from the CMS entirely (API-only).

## Next Steps

- **[Actions](/ui/admin/actions)** — Action dialogs and input forms
- **[Table Views](/ui/admin/table-views)** — View projections and toolbar
- **[Inline Editing](/ui/admin/inline-editing)** — How guards affect editability
