# Instatic’s Capability-Based Access Control System: How It Works and How to Use It

> Explore Instatic's capability-based access control. Learn how its permission strings, role mapping, and runtime guards secure server APIs, UIs, and plugin SDKs.

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

---

**Instatic implements a capability-based access control (CBAC) model that defines fine-grained permission strings in a centralized registry, maps them to system roles, and enforces authorization through type-safe runtime guards across server APIs, admin UIs, and plugin SDK endpoints.**

The CoreBunch/Instatic repository uses a **capability-based access control system** to govern every actionable operation within the platform. Instead of hard-coding role checks throughout the codebase, the system maintains a single source-of-truth array of capability strings that can be assigned to roles and verified at runtime.

## The Capability Registry: CORE_CAPABILITIES

The foundation of Instatic’s permission model lives in **[src/core/capabilities.ts]**. This module exports the `CORE_CAPABILITIES` array, which enumerates every discrete permission available in the system.

- **Shared type safety**: The same file exports the `CoreCapability` TypeScript type, ensuring both client and server code use identical permission definitions.
- **Automatic propagation**: Adding a new string to `CORE_CAPABILITIES` immediately makes it available throughout the codebase without requiring additional configuration changes.
- **Domain organization**: Capabilities are logically grouped by functional area (site editing, media management, runtime configuration, plugins, data workspace, and AI features) as documented in **[docs/reference/capabilities.md]**.

## System Roles and Permission Mapping

Role definitions reside in **[server/auth/capabilities.ts]**, which exports the `SYSTEM_ROLES` constant. On every server boot, the system synchronizes these roles using `FORCE_SYNC_ROLE_IDS` to ensure consistency.

The platform defines four built-in roles with distinct capability sets:

- **Owner**: Receives the complete capability set via `[...CORE_CAPABILITIES]`, granting unrestricted access.
- **Admin**: Uses a curated subset called `adminCapabilities`, hand-picked to prevent silent privilege escalation.
- **Client**: A lightweight editor role possessing only essential capabilities for content management.
- **Member**: A public-facing account with no default capabilities, effectively read-only until explicitly granted permissions.

## Runtime Enforcement and Type-Safe Guards

Authorization checks rely on helper functions exported from **[server/auth/capabilities.ts]**:

- **`isCoreCapability(cap)`**: Validates whether a string exists in the registry.
- **`normalizeCapabilities(caps)`**: Deduplicates and validates capability arrays.
- **`roleHasCapability(userCaps, requiredCap)`**: Performs the actual permission check.

These guards appear in server route definitions, UI command registrations, and plugin SDK route registration (`api.cms.routes.get(path, capability, handler)`). A typical enforcement pattern looks like:

```typescript
if (!roleHasCapability(user.capabilities, 'site.structure.edit')) {
  throw new ApiError(403, 'Insufficient capability')
}

```

## Implementation Examples

### Checking Permissions on Server Routes

In **[server/auth/capabilities.ts]**, capabilities act as gatekeepers on API endpoints. The following example demonstrates how to protect a page move operation:

```typescript
import { api } from '@core/http'
import { roleHasCapability } from '@core/capabilities'
import { getUser } from '@/server/auth/session'

api.cms.routes.post(
  '/pages/:id/move',
  'site.structure.edit',               // <-- capability gate
  async (req, res) => {
    const user = await getUser(req)
    if (!roleHasCapability(user.capabilities, 'site.structure.edit')) {
      return res.status(403).json({ error: 'Missing capability' })
    }
    // …perform move operation
  },
)

```

### Rendering the Capability Picker in Admin UI

The **[src/admin/pages/users/utils/capabilities.ts]** module provides metadata for the admin interface. You can render a complete permission editor using the `CORE_CAPABILITIES` registry:

```typescript
import { CORE_CAPABILITIES } from '@core/capabilities'
import { useUserStore } from '@/admin/store'

// Render a list of checkboxes for a role editor
export const CapabilityPicker = () => {
  const { role } = useUserStore()
  return (
    <ul>
      {CORE_CAPABILITIES.map((cap) => (
        <li key={cap}>
          <label>
            <input
              type="checkbox"
              checked={role.capabilities.includes(cap)}
              onChange={() => toggleCapability(cap)}
            />
            {cap}
          </label>
        </li>
      ))}
    </ul>
  )
}

```

### Dynamically Assigning Capabilities

To modify role permissions at runtime, use `normalizeCapabilities` to safely update database records:

```typescript
import { normalizeCapabilities } from '@core/capabilities'

// Assume `role` is a mutable DB row
async function addCapability(roleId: string, newCap: string) {
  const role = await db.getRole(roleId)
  const caps = normalizeCapabilities([...role.capabilities, newCap])
  await db.updateRole(roleId, { capabilities: caps })
}

```

### Plugin SDK Integration

Plugins declare required capabilities in **[src/core/plugin-sdk/capabilities.ts]**, mapping them to human-readable metadata. The SDK enforces these declarations when registering routes or exposing functionality to the core platform.

## Summary

- **Centralized registry**: The `CORE_CAPABILITIES` array in **[src/core/capabilities.ts]** serves as the single source of truth for all permissions.
- **Immutable role definitions**: **[server/auth/capabilities.ts]** defines owner, admin, client, and member roles with carefully curated capability sets.
- **Type-safe enforcement**: Runtime guards like `roleHasCapability` and `isCoreCapability` prevent unauthorized access while maintaining compile-time safety.
- **Cross-platform coverage**: The same capability strings govern server APIs, admin UI elements, and plugin SDK endpoints.
- **Extensible model**: Adding new permissions requires only updating the central registry and assigning the capability to appropriate roles.

## Frequently Asked Questions

### How do I add a new permission to Instatic?

Navigate to **[src/core/capabilities.ts]** and append your new permission string to the `CORE_CAPABILITIES` array. This automatically updates the `CoreCapability` TypeScript type. Then assign the new capability to relevant roles in **[server/auth/capabilities.ts]** and document it in **[docs/reference/capabilities.md]**.

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

The **Owner** role receives all capabilities via the spread operator `[...CORE_CAPABILITIES]`, granting complete system access. The **Admin** role uses a manually curated array called `adminCapabilities` that excludes potentially dangerous permissions, preventing accidental privilege escalation in day-to-day administrative tasks.

### How does capability checking work for plugins?

Plugins declare required capabilities in **[src/core/plugin-sdk/capabilities.ts]** when registering routes through `api.cms.routes.get()` or `api.cms.routes.post()`. The SDK automatically rejects requests from users lacking the specified capability before invoking the handler function.

### Can I create custom roles beyond the built-in system roles?

Yes. While **[server/auth/capabilities.ts]** defines the immutable `SYSTEM_ROLES`, you can create custom roles at runtime using `normalizeCapabilities()` to validate capability arrays. Store these in your database and check permissions using `roleHasCapability()` in your application code.