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 settingscms:write— Modify contentcms:publish— Publish changes to productionplugin: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 viacreateSessionafter credential validation. - Capabilities are stored as JSON arrays in the
capabilities_jsoncolumn, separating identity from permissions. - Authorization is enforced by middleware in
server/auth/authz.tsusing theauthorizefunction 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.tsand follows database conventions fromdocs/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →