# How Access Control and Capabilities Work in Instatic: RBAC, 36 Permissions, and MFA

> Understand Instatic's access control and capabilities. Learn how RBAC, 36 granular permissions, and MFA secure your system and sensitive operations.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-03

---

**Instatic implements a role-based access control (RBAC) system where 36 fine-grained capabilities are assigned to system roles, with MFA enforcement and step-up authentication protecting sensitive operations.**

Instatic (CoreBunch/Instatic) uses a capability-based security model that grants permissions through system roles rather than individual user assignments. The architecture separates concerns into distinct modules for capability definitions, authorization guards, and multi-factor authentication, ensuring that every API endpoint can enforce precise access controls.

## Understanding Instatic's Capability-Based Security Model

The foundation of Instatic's access control resides in the `@core/capabilities` package, which serves as the single source of truth for all 36 capability identifiers. Server-side code imports this canonical list and extends it with system role definitions in [`server/auth/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/capabilities.ts).

This design ensures that capability strings remain consistent across the frontend and backend, preventing drift between what the UI checks and what the API enforces.

## The 36 Core Capabilities and System Roles

### Capability Registry (@core/capabilities)

All possible permissions live in the core package as a master list. The server imports these via:

```typescript
// server/auth/capabilities.ts
import { CORE_CAPABILITIES, type CoreCapability } from '@core/capabilities'

```

This import provides the complete set of 36 capabilities that any role can potentially hold.

### System Role Definitions (server/auth/capabilities.ts)

Instatic defines four built-in system roles within the `SYSTEM_ROLES` array. Each role receives a specific subset of capabilities:

- **Owner** (`owner`): Receives every capability via the spread operator (`...CORE_CAPABILITIES`). This role has full-system access and automatically inherits new capabilities on server boot.
- **Admin** (`admin`): Holds a hard-coded list including `dashboard.read`, `site.read`, `site.structure.edit`, `users.manage`, and others. The Admin role **cannot** manage roles.
- **Client** (`client`): Limited to content editing capabilities such as `site.content.edit`, `media.read`, and `data.custom.tables.read`. Cannot modify structure or styles.
- **Member** (`member`): Public-facing account with no administrative capabilities by default.

The `FORCE_SYNC_ROLE_IDS` array ensures that Owner and Admin roles automatically receive capability updates:

```typescript
export const FORCE_SYNC_ROLE_IDS: readonly string[] = [OWNER_ROLE_ID, ADMIN_ROLE_ID]

```

### Capability Helper Functions

[`server/auth/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/capabilities.ts) exports utility functions for permission validation:

- **`roleHasCapability(capabilities, capability)`**: Checks whether a role's capability array contains a specific permission string.
- **`normalizeCapabilities(value)`**: Sanitizes incoming capability lists by filtering out unknown strings and sorting them according to the master list, preventing injection attacks.

## Authorization Guards and Runtime Checks

### The Authz Module (server/auth/authz.ts)

Runtime enforcement occurs in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts), which exports guard functions that combine authentication and authorization checks.

**`requireCapability(req, db, capability)`** resolves the session cookie to an `AuthUser`, verifies the user possesses the required capability, and returns either the user object or a `Response` object indicating failure.

**`requireAnyCapability(req, db, capabilities[])`** validates that the user holds at least one capability from the supplied list, useful for endpoints that accept multiple permission paths.

**`requireAuthenticatedUser(req, db)`** handles session resolution and MFA enforcement. If the user's account requires MFA but the session hasn't completed it, the function returns a `401` response with `{error: 'mfa_required'}`.

### Step-Up Authentication with requireStepUp

Sensitive operations (user deletion, role management, device revocation) require fresh verification through step-up authentication. The **`requireStepUp(req, db, user)`** function checks the `step_up_expires_at` column on the session row:

```typescript
export async function deleteUserHandler(req: Request, db: DbClient) {
  const user = await requireCapability(req, db, 'users.manage')
  if (user instanceof Response) return user

  const stepUp = await requireStepUp(req, db, user)
  if (stepUp) return stepUp

  // Proceed with deletion...
}

```

If the step-up window expired, the function returns `401 {error: 'step_up_required'}`, forcing the user to re-enter their password.

## Multi-Factor Authentication (MFA) Implementation

### TOTP Secret Generation and Verification

The [`server/auth/mfa.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/mfa.ts) module handles Time-Based One-Time Password (TOTP) implementation:

- **`generateTotpSecret()`**: Creates a cryptographically secure base-32 secret.
- **`totpProvisioningUri({issuer, accountName, secret})`**: Generates an `otpauth://` URL compatible with Google Authenticator and Authy for QR code generation.
- **`verifyTotpCode(secret, code, now?)`**: Validates a 6-digit code against the current and adjacent time windows using constant-time comparison to prevent timing attacks.

### Recovery Codes and Session Management

Instatic provides fallback authentication through recovery codes:

- **`generateRecoveryCodes()`**: Creates a set of one-time-use backup codes.
- **`hashRecoveryCode(code)`**: Returns a SHA-256 hash for secure storage.
- **`findMatchingRecoveryCodeHash(hashes, code)`**: Performs constant-time lookup of recovery codes.

The session layer ([`server/auth/sessions.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/sessions.ts)) tracks MFA requirements via `sessionRequiresMfa` and manages step-up expiry through `getSessionStepUpExpiresAt`, ensuring that elevated privileges time out appropriately.

## Request Lifecycle: From Authentication to Authorization

Instatic processes every secured request through a layered pipeline:

1. **Session Resolution**: `requireAuthenticatedUser` extracts and hashes the session cookie, validating the user identity and checking MFA status.
2. **Capability Verification**: `requireCapability` validates that the user's role includes the necessary permission from the 36-capability master list.
3. **Step-Up Validation**: For sensitive operations, `requireStepUp` verifies the step-up window hasn't expired.
4. **Response Handling**: All guard functions return `Response` objects on failure, allowing handlers to exit early with standardized error codes.

This orthogonal design keeps authentication, authorization, and MFA concerns separate, making each component independently testable.

## Summary

- Instatic defines **36 fine-grained capabilities** in `@core/capabilities`, imported by the server to maintain a single source of truth.
- **Four system roles** (Owner, Admin, Client, Member) receive capability assignments in [`server/auth/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/capabilities.ts), with Owner and Admin force-synced on every boot.
- **Authorization guards** (`requireCapability`, `requireAnyCapability`) in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) enforce runtime permissions and return standardized responses on failure.
- **Step-up authentication** requires fresh password entry for sensitive operations, checked via `requireStepUp` against the session's `step_up_expires_at` timestamp.
- **MFA implementation** uses TOTP with `verifyTotpCode` and recovery codes with constant-time comparison functions in [`server/auth/mfa.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/mfa.ts).

## Frequently Asked Questions

### How many capabilities does Instatic support?

Instatic supports **36 distinct capabilities** defined in the `@core/capabilities` package. This list includes permissions for dashboard access, site structure editing, user management, media handling, and custom data operations. The master list ensures consistent permission naming across the entire application.

### What is the difference between Owner and Admin roles?

The **Owner** role automatically receives all 36 capabilities via the spread operator (`...CORE_CAPABILITIES`), including role management permissions. The **Admin** role has a hard-coded capability list that excludes role management but includes user management and site structure editing. Only Owner and Admin are force-synced via `FORCE_SYNC_ROLE_IDS` to receive new capabilities automatically.

### How does step-up authentication work?

Step-up authentication requires users to re-enter their password for sensitive operations. When `requireStepUp` is called, it checks the `step_up_expires_at` column in the session record. If the timestamp has passed, the function returns a `401` error with `{error: 'step_up_required'}`, forcing the client to recollect the password before proceeding with actions like user deletion or role changes.

### What MFA methods does Instatic support?

Instatic supports **TOTP (Time-Based One-Time Password)** through authenticator apps like Google Authenticator and Authy, plus **recovery codes** for account recovery. The system uses `generateTotpSecret` for setup, `verifyTotpCode` for validation with constant-time comparison, and `generateRecoveryCodes` for backup access. MFA requirements are enforced at the session layer via `sessionRequiresMfa`.