Instatic's 38-Capability Access Control System: Implementation and Enforcement

Instatic implements a fine-grained capability-based access control model that defines 38 distinct permissions in src/core/capabilities.ts, enforced through the hasCapability() helper in src/admin/access.ts and middleware validation in server/auth/authz.ts.

The CoreBunch/Instatic content management system relies on a comprehensive 38-capability access control system to govern user interactions across the admin interface and API. This capability-based architecture assigns atomic permissions—such as site.structure.edit and content.publish.any—that combine to create precise access profiles. By centralizing these 38 capabilities in a type-safe registry, Instatic ensures consistent authorization enforcement on both client and server.

Core Capability Registry and Type Safety

The foundation of Instatic's permission model rests in src/core/capabilities.ts, which exports the master list of all 38 capabilities.

The CORE_CAPABILITIES Constant

This file defines CORE_CAPABILITIES, a constant array containing every capability string supported by the system. This array serves as the single source of truth for authorization logic throughout the application, covering domains from site editing and media management to plugin installation and AI features.

The CoreCapability Type

Derived directly from the CORE_CAPABILITIES array, the CoreCapability TypeScript type provides compile-time safety. When functions require a specific permission, they accept parameters of type CoreCapability, preventing invalid capability strings from entering the system and ensuring that the 38-capability set remains the exclusive vocabulary for access control.

Client-Side Permission Checking with hasCapability()

All UI authorization logic flows through utility functions defined in src/admin/access.ts.

The hasCapability() Function

The primary validation helper checks user sessions against required permissions:

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

Semantic Permission Predicates

Built atop hasCapability(), higher-level predicates clarify intent throughout the admin codebase:

  • canEditSiteStructure(user) validates site.structure.edit
  • canPublishContent(user) validates content.publish.any
  • canManagePlugins(user) validates plugins.install

Conditional UI Rendering Examples

The admin interface uses these helpers to control visibility. In src/admin/state/useSiteSummary.ts, the application checks hasCapability(currentUser, 'site.read') before rendering site data. The capability picker in src/admin/pages/users/utils/capabilities.ts references CORE_CAPABILITIES metadata to disable checkboxes for permissions the current user cannot grant, ensuring the 38-capability system governs even the permission-assignment interface itself.

Server-Side Enforcement Architecture

Session Capability Attachment

During authentication, server/auth/capabilities.ts queries the database to retrieve a user's assigned capabilities from the 38-capability set and attaches them to the session object. This makes the permission data available for subsequent request authorization without repeated database lookups.

AuthZ Middleware Protection

The router middleware in server/auth/authz.ts extracts capabilities from the encrypted session cookie and validates them against route requirements. If a request lacks the declared capability—such as media.write for upload endpoints—the middleware throws an ApiError and terminates the request before reaching the handler. This ensures that client-side bypass attempts cannot compromise the 38-capability access control boundaries.

Plugin SDK and Capability Integration

The plugin architecture extends the 38-capability system to third-party extensions through src/core/plugin-sdk/types/serverApi.ts.

Route-Level Capability Requirements

Plugin routes declare required capabilities using the api.cms.routes methods, ensuring third-party code respects the same authorization boundaries:

api.cms.routes.get(path, capability, handler)

This signature allows plugins to specify which of the 38 capabilities—such as plugins.read or plugins.install—are required to access their functionality.

Practical Code Examples

Checking permissions in admin UI components:

import { hasCapability } from '@admin/access'

if (hasCapability(currentUser, 'media.write')) {
  // render "Upload Media" button
}

Protecting API endpoints with capability requirements:

// server/auth/routes.ts
api.cms.routes.post(
  '/api/media/upload',
  'media.write',               // required capability from the 38-capability set
  async (req, res) => {
    // handler executes only for authorized users
  }
)

Plugin route registration with capability gating:

// src/core/plugin-sdk/types/serverApi.ts
api.cms.routes.get(
  '/my-plugin/health',
  'plugins.read',              // gates access to plugin functionality
  async (req, res) => { /* ... */ }
)

Summary

  • The 38-capability access control system is centralized in src/core/capabilities.ts via the CORE_CAPABILITIES array and CoreCapability type.
  • Client-side checks use the hasCapability() helper in src/admin/access.ts to toggle UI elements based on permissions like site.read or content.publish.any.
  • Server-side enforcement occurs in server/auth/authz.ts, which validates session-attached capabilities before executing route handlers.
  • Semantic predicates such as canEditSiteStructure() abstract common permission patterns for cleaner business logic.
  • The plugin SDK integrates with the capability system through src/core/plugin-sdk/types/serverApi.ts, requiring explicit capability declarations for all plugin routes.
  • Test coverage in capability-picker-coverage.test.ts ensures every capability maintains appropriate UI picker metadata.

Frequently Asked Questions

Where are the 38 capabilities defined in Instatic?

The complete set of 38 capabilities is defined as the CORE_CAPABILITIES constant array in src/core/capabilities.ts. This file also exports the CoreCapability TypeScript type, which provides compile-time validation by restricting strings to those present in the array.

How does the hasCapability() function work?

Located in src/admin/access.ts, hasCapability() accepts a CmsCurrentUser object and a CoreCapability string. It returns true if the user's capabilities array includes the requested permission, powering both individual checks and higher-level predicates like canManagePlugins().

What prevents API access if UI capability checks are disabled?

Server-side middleware in server/auth/authz.ts performs canonical authorization by extracting capabilities from the session cookie and comparing them against route requirements. If the user lacks the required capability—such as plugins.install for plugin management—the middleware rejects the request with an ApiError before the handler executes.

How do plugins interact with the 38-capability system?

Plugins declare required capabilities when registering routes through the SDK in src/core/plugin-sdk/types/serverApi.ts. The api.cms.routes methods accept a capability parameter (e.g., plugins.read) that the auth middleware enforces, ensuring third-party extensions respect the same access control boundaries as core functionality.

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 →