How Instatic Implements Authentication and Capability-Based Access Control

Instatic combines session-based authentication with fine-grained capability tokens stored in a JSON column, enforced by middleware in 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 and 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. 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. The server returns this token as an HTTP-only, secure cookie.

// 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, 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. 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. 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.

// 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:

// 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:

// 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. 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 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, 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 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 and follows database conventions from 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, establishing a session token. Authorization occurs afterward via the authorize function in 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, 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. 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →