How Instatic Implements Access Control with Its 38-Capability System

Instatic enforces fine-grained permissions through a centralized capability array (CORE_CAPABILITIES) defined in src/core/capabilities.ts, with runtime checks performed by the hasCapability() helper in src/admin/access.ts and enforced server-side via middleware in server/auth/authz.ts.

The Instatic CMS, maintained in the CoreBunch/Instatic repository, employs a comprehensive 38-capability access control model that governs every permission from site structure editing to AI feature access. This system centralizes all authorization logic in a type-safe registry, ensuring that both the admin interface and backend API endpoints remain synchronized on user permissions.

The Core Capability Registry

All 38 permissions are enumerated as string constants in src/core/capabilities.ts. This file exports CORE_CAPABILITIES, a master array that serves as the single source of truth, and derives the TypeScript union type CoreCapability from it. The capabilities span domains including site editing, media management, runtime configuration, storage, plugins, user and role administration, audit logging, data workspaces, and AI features.

Adding a new permission requires appending a string to this array; the type system automatically propagates the change to all consumers. The accompanying test in src/__tests__/architecture/capability-picker-coverage.test.ts guarantees that every entry includes UI picker metadata, preventing orphaned capabilities.

Client-Side Permission Validation

When a user logs in, the server attaches their capability list to the session (handled in server/auth/capabilities.ts). The admin UI then uses utility helpers from src/admin/access.ts to determine visibility of actions and navigation elements.

The primary check is the hasCapability() function:

export function hasCapability(user: CmsCurrentUser | null, capability: CoreCapability): boolean {
  return user?.capabilities?.includes(capability) ?? false
}

Higher-level predicates build on this foundation for common operations:

  • canEditSiteStructure(user) → hasCapability(user, 'site.structure.edit')
  • canPublishContent(user) → hasCapability(user, 'content.publish.any')
  • canManagePlugins(user) → hasCapability(user, 'plugins.install')

Components like the site summary view in src/admin/state/useSiteSummary.ts invoke these helpers to gate data fetching, while src/admin/pages/users/utils/capabilities.ts uses the metadata from CORE_CAPABILITIES to disable checkboxes the current user cannot grant.

Server-Side Enforcement

Authorization is not merely a UI concern. The middleware in server/auth/authz.ts extracts the capability list from the session cookie on every request and validates it against the route's required permission. If the user lacks the capability, the middleware throws an ApiError, rejecting the request before it reaches the handler.

Route definitions specify their required capability as a parameter:

// server/auth/routes.ts
api.cms.routes.post(
  '/api/media/upload',
  'media.write',               // capability required
  async (req, res) => {
    // handler runs only if the user holds `media.write`
  }
)

This ensures that even if a client bypasses UI checks, the backend will reject unauthorized requests targeting sensitive endpoints.

Plugin Integration

Plugins leverage the same system for their own routes through the SDK defined in src/core/plugin-sdk/types/serverApi.ts. When registering a route, plugins declare which existing capability guards it:

api.cms.routes.get(
  '/my-plugin/health',
  'plugins.read',              // only users with this capability can call it
  async (req, res) => { /* ... */ }
)

The api.cms.routes helper wires these requirements into the router, subjecting plugin endpoints to the same server/auth/authz.ts validation as native CMS routes.

Summary

  • The CORE_CAPABILITIES array in src/core/capabilities.ts serves as the single source of truth for all 38 permissions, grouped by functional domain.
  • The hasCapability() helper in src/admin/access.ts provides the primary client-side check, returning a boolean based on the user's session data.
  • Server-side middleware in server/auth/authz.ts validates capabilities on every API request, rejecting unauthorized access with an ApiError.
  • Plugins register protected routes through api.cms.routes with explicit capability requirements defined in src/core/plugin-sdk/types/serverApi.ts, ensuring third-party extensions adhere to the same security model.

Frequently Asked Questions

What are the 38 capabilities in Instatic?

The capabilities cover functional domains including site structure editing, content publishing, media management, plugin installation, user and role administration, audit logging, data workspace operations, and AI features. The complete, type-safe list is maintained as the CORE_CAPABILITIES constant array in src/core/capabilities.ts.

How does Instatic check user permissions in the admin UI?

Admin components import the hasCapability() function from src/admin/access.ts to conditionally render UI elements. For example, the site summary view checks hasCapability(currentUser, 'site.read') before rendering data, while higher-level helpers like canEditSiteStructure() wrap this logic for specific editorial actions.

Can plugins define their own custom capabilities?

While the base system provides 38 core capabilities, plugins register routes through the SDK in src/core/plugin-sdk/types/serverApi.ts by specifying which existing capability is required (e.g., api.cms.routes.get(path, 'plugins.read', handler)). The middleware in server/auth/authz.ts enforces these requirements regardless of whether the route is native or plugin-provided, ensuring consistent enforcement.

How is the capability system tested for completeness?

The architecture test suite in src/__tests__/architecture/capability-picker-coverage.test.ts verifies that every string in CORE_CAPABILITIES has corresponding metadata for the UI picker component. This guarantees that new capabilities added to the array cannot be assigned to users without interface support, preventing configuration drift.

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 →