# Understanding Instatic's Capability-Based Permission System and Role Assignment

> Learn about Instatic's capability-based permission system. Understand how fine-grained string capabilities like site.read are grouped into roles and assigned to users for secure operations.

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

---

**Instatic secures every privileged operation using fine-grained string capabilities (e.g., `site.read`, `media.upload`) that are grouped into roles and assigned to users, with enforcement implemented at both the server API route and UI command layers.**

CoreBunch's Instatic implements a capability-based permission model that replaces traditional coarse-grained access control with granular permission strings. This system allows administrators to compose custom roles from atomic capabilities, ensuring users receive only the specific permissions required for their tasks. The architecture protects core system tables while allowing plugins to request additional capabilities through their manifest files.

## Core Components of the Permission System

### The Capability Registry

All core capabilities are enumerated in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts). Each entry defines a permission string, a human-readable label, and a description. The registry serves as the single source of truth for what actions can be guarded throughout the application.

```typescript
// src/core/capabilities.ts
export const SITE_READ_CAPABILITY = {
  permission: 'site.read',
  label: 'Read site data',
  description: 'Allows reading page content, layout and settings.',
};

```

### Type Definitions and Builders

The mapping from permission strings to capability objects lives in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts), with builder utilities in [`src/core/plugin-sdk/builders/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/permissions.ts). These files expose runtime helpers such as `isCapability(value)` and `capabilityLabel(permission)` that validate and display capabilities across the codebase.

### Role Definition and Storage

A role is a database record stored in the `roles` table, containing an array of capability strings. The role schema is defined in [`src/core/data/systemTableGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/systemTableGuard.ts), which protects system tables as frozen entities for every user. The user schema in [`src/core/data/users.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/users.ts) (created by the migration system) stores a `role` column that references the role record.

## Enforcing Capabilities Across the Stack

### Server-Side Route Protection

All server plugin routes declare a required capability as the second argument to the route registration method. The enforcement code lives in [`src/core/plugin-sdk/types/serverApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/serverApi.ts) and is applied by [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) when processing requests.

```typescript
// src/server/handlers/cms/pages.ts
api.cms.routes.patch(
  '/admin/pages/:id',
  'pages.edit',                // Capability required
  async (req, res) => {
    // …handler logic…
  }
);

```

If the current session lacks `pages.edit`, the runtime in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) returns a **403 Forbidden** error.

### UI Gating and Client-Side Checks

UI commands and menu items declare a `capability` field that automatically hides or disables actions the current user does not possess. This pattern appears throughout `src/admin/spotlight/commands/*.ts`.

```typescript
// src/admin/spotlight/commands/pages.ts
{
  id: 'pages.edit',
  title: 'Edit Page',
  capability: 'pages.edit',   // UI hides this command if missing
  action: async (ctx) => { /* open editor */ }
}

```

### Runtime Capability Validation

Plugin code frequently uses the `hasCapability` helper to guard mutations, file uploads, and AI calls.

```typescript
import { hasCapability } from '@core/permissions';

if (!hasCapability(session, 'media.upload')) {
  throw new Error('Missing capability: media.upload');
}

```

## Role Assignment Workflow

Roles are created, persisted, and assigned through a coordinated flow between the admin UI and server handlers:

1.  **Create a role** – Administrators select capabilities from the registry via the dialog in [`src/admin/pages/users/roleForm.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/roleForm.tsx). The component POSTs to `/admin/roles` with `name`, `description`, and `capabilities[]`.

2.  **Persist the role** – The server handler in [`src/server/handlers/cms/roles.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/handlers/cms/roles.ts) validates the payload against the role schema and writes the record to the database.

3.  **Assign to user** – In [`src/admin/pages/users/userForm.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/users/userForm.tsx), admins select a role from a dropdown. The selected role ID is stored in the user's `role` column.

4.  **Load on login** – [`src/server/auth/session.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/auth/session.ts) loads the user's role, expands the stored capability strings, and attaches them to the session cookie.

5.  **Verify on request** – Every request passes through [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts), which checks the required capability against the session's capability set before executing the handler.

## Dynamic Capability Assignment for Plugins

Plugins can request additional capabilities via their [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) manifest using the `contentAccess[]` array. The permission system maps these declarations to core capabilities, ensuring a plugin can only act on the data types it explicitly declares.

```json
// plugin.json
{
  "contentAccess": ["pages", "media"]
}

```

## Summary

-   **Capabilities** are fine-grained string identifiers defined in [`src/core/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/capabilities.ts) that control access to specific operations like `site.read` or `users.manage`.
-   **Roles** are database records storing arrays of capabilities, defined in the frozen system tables guarded by [`src/core/data/systemTableGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/systemTableGuard.ts).
-   **Enforcement** occurs at the route level in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts), at the UI level via capability declarations in command files, and in code via the `hasCapability()` helper.
-   **Assignment** flows from role creation in [`roleForm.tsx`](https://github.com/CoreBunch/Instatic/blob/main/roleForm.tsx), through persistence in [`roles.ts`](https://github.com/CoreBunch/Instatic/blob/main/roles.ts), to user assignment in [`userForm.tsx`](https://github.com/CoreBunch/Instatic/blob/main/userForm.tsx), with session loading handled by [`session.ts`](https://github.com/CoreBunch/Instatic/blob/main/session.ts).

## Frequently Asked Questions

### How do I check if a user has a specific capability in my plugin code?

Import the `hasCapability` function from `@core/permissions` and pass the session object along with the capability string. The function returns a boolean indicating whether the session's role includes the requested permission.

```typescript
import { hasCapability } from '@core/permissions';

if (hasCapability(session, 'media.upload')) {
  // Allow upload
}

```

### Can plugins define their own custom capabilities?

Plugins cannot create arbitrary new capabilities. Instead, they request access to existing core capabilities via the `contentAccess` array in their [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) manifest. The system maps these content types to the appropriate core capabilities (e.g., `pages.read`, `media.upload`) based on what the plugin declares it needs to access.

### What happens if a user attempts to access a protected route without the required capability?

When [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) processes a request, it compares the capability required by the route registration (e.g., `api.cms.routes.get('/admin/users', 'users.manage', handler)`) against the session's capability set. If the capability is missing, the runtime immediately returns a **403 Forbidden** response before the route handler executes.

### Where are role definitions stored and how are they protected?

Role definitions are stored in the `roles` system table, with schemas defined in both [`src/core/data/systemTableGuard.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/systemTableGuard.ts) and migration files. These system tables are frozen, meaning their structure cannot be modified by regular users or plugins, ensuring the integrity of the permission system's foundational data structures.