# 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 (admin-only by default)

The CMS is restricted to Better Auth platform admins (`user.role === "admin"`) by default — non-admin authenticated users are redirected to `/account/profile`. To allow any authenticated user, opt out explicitly:

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

Under the default `access: "admin"`, the compiler emits server-side gates on every CMS-facing surface so a non-admin can't reach the CMS even by bypassing the SPA:

1. **Account UI** — the "Go to CMS" button is hidden.
2. **CMS SPA shell** — the Worker checks `ctx.userRole === "admin"` before serving `index.html`. Unauthenticated requests are redirected to login; authenticated non-admins get `403 Admin access required`.
3. **`/api/v1/schema`** — the CMS metadata endpoint returns `403` to non-admins.
4. **`custom_view` routes** — the CMS-internal saved-views table uses `userRole: ["admin"]` (see [`userRole`](/compiler/definitions/access#userrole-vs-roles)), so saved views can't be read or modified from outside the CMS by non-admin 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 header includes a **role switcher** (in demo mode) or reads the user's actual role from their organization membership (in live mode). 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](/stack/cms/actions)** — Action dialogs and input forms
- **[Table Views](/stack/cms/table-views)** — View projections and toolbar
- **[Inline Editing](/stack/cms/inline-editing)** — How guards affect editability
