# How Instatic Implements Its 38-Capability Access Control System

> Discover how Instatic implements its 38-capability access control system using server-side helpers and client-side utilities. Learn about role mapping and admin synchronization.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-26

---

**Instatic's access control system centralizes 38 granular permissions in a single TypeScript constant exported from [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts), enforcing them through server-side helpers like `requireCapability` and client-side utilities like `hasCapability` while mapping them to four built-in roles with automatic synchronization for system administrators.**

Instatic, an open-source CMS by CoreBunch, implements a fine-grained access control system using exactly 38 capability strings. This article examines how the Instatic access control system maintains a single source of truth for these permissions, synchronizes them across server handlers and React components, and prevents unauthorized access through architecture-level enforcement.

## The Core Capability Catalog

The foundation of the system resides in **[`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts)**, which exports the `CORE_CAPABILITIES` constant containing all 38 permission strings. This file serves as the single source of truth, with the `CoreCapability` type derived as `typeof CORE_CAPABILITIES[number]` to ensure type safety across the codebase.

The 38 capabilities range from broad read-only flags (`dashboard.read`, `site.read`) to granular controls for media management, data workspaces, plugin administration, and AI features. Each capability is documented in **[`docs/reference/capabilities.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/capabilities.md)**, which includes a matrix mapping which built-in roles receive specific permissions by default.

## Server-Side Capability Checks

All HTTP handlers enforce authorization through helpers defined in **[`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts)**. These functions validate the user's capability array stored in the database against the required permissions:

- **`requireCapability(req, db, capability)`** – Verifies the logged-in user holds a specific capability string, returning the authenticated user object or a 403 Response.
- **`requireAnyCapability(req, db, capabilityArray)`** – Allows access if the user holds any capability in the provided array (e.g., `SITE_WRITE_CAPABILITIES`).
- **`requireAuthenticatedUser(req, db)`** – Validates only the session exists without checking specific capabilities.

The architecture test suite in **[`src/__tests__/architecture/cms-handlers-capability-gated.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/architecture/cms-handlers-capability-gated.test.ts)** automatically scans all handlers under `server/handlers/cms/**` to verify they invoke one of these authorization helpers, preventing accidental bypasses.

```typescript
// src/server/handlers/cms/site.ts
import { requireCapability } from '../../auth/authz';

if (req.method === 'GET') {
  const user = await requireCapability(req, db, 'site.read');
  if (user instanceof Response) return user;   // 403 if missing
  // … fetch and return site data
}

```

## Client-Side Access Control

The React admin UI implements parallel checks in **[`src/admin/access.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/access.ts)** through the `hasCapability(user, capability)` function. This thin wrapper checks `user?.capabilities.includes(capability)` and returns a boolean for conditional rendering.

Higher-level convenience functions group related capabilities for common UI patterns:

- **`canEditStructure(user)`** – Checks structural modification rights
- **`canReadMedia(user)`** – Verifies media access permissions
- **`canAccessWorkspace(user, workspace)`** – Determines workspace visibility

```tsx
import { useAdminSession } from '@admin/session';
import { hasCapability } from '@admin/access';

export function MediaUploadButton() {
  const { user } = useAdminSession();
  if (!hasCapability(user, 'media.write')) return null; // hide for unauthorized users
  return <Button onClick={openUploader}>Upload Media</Button>;
}

```

## Role Definitions and Synchronization

Built-in system roles are defined in **[`server/auth/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/capabilities.ts)** and mapped to capability subsets:

- **Owner** – Receives all 38 capabilities automatically
- **Admin** – Receives all capabilities except `roles.manage`
- **Client** – Minimal subset including read-only dashboard, site view, content edit, and media read
- **Member** – No capabilities granted by default

The `syncSystemRoles(db)` function executes on every server start, force-synchronizing the Owner and Admin role definitions. This ensures that newly added capabilities in `CORE_CAPABILITIES` automatically propagate to these system roles without manual database migration. Custom roles and the Client/Member roles retain their existing capability sets until explicitly modified through the Roles UI.

## Plugin Permissions vs. Core Capabilities

The system distinguishes between **core capabilities** governing human users in the admin UI and **plugin permissions** controlling code execution within the QuickJS sandbox. Plugin permissions are declared in a plugin's [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) file and enforced separately by the sandbox runtime.

However, plugins can still gate routes on core capabilities using the same enforcement mechanism:

```typescript
// Plugin route registration
api.cms.routes.get('/custom-data', 'data.read', async (req, res) => {
  // Handler only executes if user has 'data.read' capability
});

```

## Step-Up Authentication for High-Impact Actions

Certain destructive operations require additional verification beyond standard capability checks. The step-up authentication flow, documented in **[`docs/features/auth-and-access.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/auth-and-access.md)**, protects high-impact actions including `plugins.install`, `storage.migrate`, and `data.import` with `replace` mode.

The `requireStepUp(req, db)` helper validates that the user has recently re-authenticated before permitting these operations, creating a dual-gate security model where capability ownership alone is insufficient.

## Architecture Enforcement and Request Flow

The complete request authorization flow follows five steps:

1. **Authentication** – Session cookie validation
2. **Capability Extraction** – Loading the user's capability array from the database based on assigned roles
3. **Gate Enforcement** – Handler invocation of `requireCapability` or `requireAnyCapability`
4. **Step-Up Validation** – Optional secondary check via `requireStepUp` for sensitive operations
5. **Authorization Result** – Request proceeds or returns 403/401

Architecture tests scan the entire codebase to ensure every CMS handler includes capability gating, making unauthorized endpoints structurally impossible to merge.

## Summary

- **Single Source of Truth**: All 38 capabilities are defined in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts) and exported as the `CoreCapability` type, preventing drift across the codebase.
- **Server Enforcement**: The `requireCapability` and `requireAnyCapability` helpers in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) provide consistent 403 handling for all HTTP handlers.
- **Client Integration**: The `hasCapability` utility and workspace helpers in [`src/admin/access.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/access.ts) enable fine-grained UI conditional rendering.
- **Role Management**: Built-in roles automatically synchronize capabilities on server startup via `syncSystemRoles`, ensuring Owners and Admins always receive new permissions.
- **Structural Guarantees**: Architecture tests enforce that every handler under `server/handlers/cms/**` implements capability checks, eliminating bypass vulnerabilities.

## Frequently Asked Questions

### What are the 38 capabilities in Instatic?

The 38 capabilities cover dashboard access (`dashboard.read`), site management (`site.read`, `site.write`), media handling (`media.read`, `media.write`), data workspace operations, plugin administration, role management (`roles.manage`), and AI features. The complete authoritative list resides in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts), with human-readable documentation available in [`docs/reference/capabilities.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/capabilities.md).

### How does Instatic prevent unauthorized API access?

Instatic prevents unauthorized access through mandatory capability checks in every HTTP handler. The server-side authorization layer in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) requires handlers to call `requireCapability` or `requireAnyCapability`, which return 403 responses for unauthorized users. Additionally, architecture tests automatically detect any handlers missing these gates, blocking their deployment.

### What is the difference between capabilities and plugin permissions?

Core capabilities control human user access to the admin UI and CMS features, stored in the user's database record and checked via `requireCapability`. Plugin permissions, declared in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json), control what sandboxed code can execute within the QuickJS environment. While distinct, plugins can optionally gate routes on core capabilities using the standard `api.cms.routes.get(path, capability, handler)` signature.

### How do I add a custom capability to Instatic?

To add a capability, append the string to the `CORE_CAPABILITIES` array in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts), then update the Owner and Admin role definitions in [`server/auth/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/capabilities.ts) to include the new capability. Finally, use the new capability in your handler via `requireCapability(req, db, 'your.new.capability')`. The `syncSystemRoles` function automatically grants the new capability to existing Owners and Admins on the next server startup.