# How Instatic Implements Authentication and Capability-Based Access Control

> Learn how Instatic implements authentication and capability based access control using session tokens and middleware for secure request processing.

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

---

**Instatic combines session-based authentication with fine-grained capability tokens stored in a JSON column, enforced by middleware in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) that checks required permissions before processing any request.**

Instatic is an open-source CMS that employs a security model distinguishing strictly between identity verification and action authorization. The system implements **capability-based access control** alongside traditional session authentication to provide granular permissions across the admin interface, API endpoints, and plugin ecosystem, as detailed in [`docs/features/auth-and-access.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/auth-and-access.md) and [`docs/reference/capabilities.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/capabilities.md).

## Authentication Flow and Session Management

### Credential Validation and Session Creation

The entry point for authentication resides in [`server/handlers/cms/auth.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/auth.ts). This handler validates user credentials through the `validateCredentials` function imported from `@/server/auth/credential`, then establishes a session via `createSession` from `@/server/auth/session`.

The session creation process accepts a user identifier and an array of capabilities (e.g., `['cms:read', 'cms:write']`), generating a cryptographically random token using logic defined in [`scripts/generate-secret-key.ts`](https://github.com/CoreBunch/Instatic/blob/main/scripts/generate-secret-key.ts). The server returns this token as an HTTP-only, secure cookie.

```typescript
// server/handlers/cms/auth.ts
import { createSession } from '@/server/auth/session';
import { validateCredentials } from '@/server/auth/credential';

export async function loginHandler(req: Request) {
  const { email, password } = await req.json();
  const user = await validateCredentials(email, password);
  const session = await createSession(user.id, ['cms:read', 'cms:write']);
  return new Response(null, {
    status: 200,
    headers: { 'Set-Cookie': `session=${session.token}; HttpOnly; Secure` },
  });
}

```

### Session Storage and the Capabilities JSON Column

Session data persists in the `sessions` table within PostgreSQL or SQLite. According to [`docs/reference/database-dialects.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/database-dialects.md), the schema stores capability arrays in a column named `capabilities_json`, following the repository's convention for JSON-structured fields.

This design separates **who** the user is from **what** they can do, allowing the system to grant different capability sets to different sessions for the same user without modifying user records.

## Capability-Based Authorization

### The Capability Model

Capabilities follow a namespaced string format documented in [`docs/reference/capabilities.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/capabilities.md). Common tokens include:
- `cms:read` — View content and settings
- `cms:write` — Modify content
- `cms:publish` — Publish changes to production
- `plugin:install` — Install or update plugins

Unlike role-based systems, Instatic checks for explicit capability presence, enabling fine-grained permission matrices where specific actions require specific tokens.

### Authorization Middleware Enforcement

The enforcement layer lives in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts). The `authorize` function extracts the session token from incoming request cookies, retrieves the associated capability set from the database, and verifies the presence of the required capability.

If the check fails, the middleware immediately returns a 403 Forbidden response without reaching the business logic.

```typescript
// server/auth/authz.ts
import { getSessionFromCookie } from '@/server/auth/session';
import { Capability } from '@/docs/reference/capabilities';

export async function authorize(req: Request, required: Capability) {
  const session = await getSessionFromCookie(req);
  if (!session?.capabilities.includes(required)) {
    return new Response('Forbidden', { status: 403 });
  }
  return null; // authorized – continue processing
}

```

## Implementing Access Control in Practice

### Protecting API Endpoints

To secure a route, handlers import the `authorize` function and specify the required capability. The following example from the publishing workflow demonstrates this pattern:

```typescript
// server/handlers/cms/publish.ts
import { authorize } from '@/server/auth/authz';

export async function publishHandler(req: Request) {
  const authError = await authorize(req, 'cms:publish');
  if (authError) return authError;

  // ... perform publish logic ...
  return new Response('Published', { status: 200 });
}

```

### Capability Checks in Plugins

Plugins declare their requirements in manifest files. The core validates these against the current session before executing plugin code, as implemented in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts):

```typescript
// src/core/plugins/manifest.ts
export function pluginCanInstall(manifest: PluginManifest) {
  return manifest.requiredCapabilities?.includes('plugin:install') ?? false;
}

```

## Security Configuration

### Secret Key Generation

Session token signing relies on secrets generated by [`scripts/generate-secret-key.ts`](https://github.com/CoreBunch/Instatic/blob/main/scripts/generate-secret-key.ts). Administrators must run this script during initial setup to create the cryptographic keys stored in environment variables, ensuring token integrity and preventing session forgery.

### Database Schema Considerations

The [`docs/reference/database-dialects.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/database-dialects.md) file specifies that JSON-stored capabilities use the `*_json` column suffix pattern. This convention ensures compatibility across PostgreSQL's `jsonb` and SQLite's `TEXT` storage types while maintaining consistent query patterns.

## Summary

- **Authentication** occurs in [`server/handlers/cms/auth.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/auth.ts), creating signed sessions via `createSession` after credential validation.
- **Capabilities** are stored as JSON arrays in the `capabilities_json` column, separating identity from permissions.
- **Authorization** is enforced by middleware in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) using the `authorize` function to check capability presence.
- **Plugins** define required capabilities in manifests, checked against active sessions before execution.
- **Security** relies on tokens generated via [`scripts/generate-secret-key.ts`](https://github.com/CoreBunch/Instatic/blob/main/scripts/generate-secret-key.ts) and follows database conventions from [`docs/reference/database-dialects.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/database-dialects.md).

## Frequently Asked Questions

### What is the difference between authentication and authorization in Instatic?

Authentication verifies user identity through the login handler in [`server/handlers/cms/auth.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/auth.ts), establishing a session token. Authorization occurs afterward via the `authorize` function in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts), which verifies whether that session possesses the specific capability required for the requested action, regardless of who the user is.

### How are capabilities stored in the database?

Capabilities are serialized as JSON arrays and stored in the `capabilities_json` column of the `sessions` table. This approach, documented in [`docs/reference/database-dialects.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/database-dialects.md), allows flexible permission sets per session while maintaining queryable storage across supported database dialects.

### Can plugins define custom capabilities?

Yes. Plugins declare required capabilities in their manifest files, typically checked via functions like `pluginCanInstall` in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts). The system validates these requirements against the active session's capability set before allowing plugin operations to execute.

### What happens when a request lacks the required capability?

The `authorize` middleware in [`server/auth/authz.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/auth/authz.ts) returns a 403 Forbidden response immediately, preventing the request from reaching business logic. This fail-closed design ensures that missing capabilities cannot be bypassed through API manipulation or direct endpoint access.