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)validatessite.structure.editcanPublishContent(user)validatescontent.publish.anycanManagePlugins(user)validatesplugins.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.tsvia theCORE_CAPABILITIESarray andCoreCapabilitytype. - Client-side checks use the
hasCapability()helper insrc/admin/access.tsto toggle UI elements based on permissions likesite.readorcontent.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.tsensures 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →