How Access Control and Capabilities Work in Instatic: RBAC, 36 Permissions, and MFA
Instatic implements a role-based access control (RBAC) system where 36 fine-grained capabilities are assigned to system roles, with MFA enforcement and step-up authentication protecting sensitive operations.
Instatic (CoreBunch/Instatic) uses a capability-based security model that grants permissions through system roles rather than individual user assignments. The architecture separates concerns into distinct modules for capability definitions, authorization guards, and multi-factor authentication, ensuring that every API endpoint can enforce precise access controls.
Understanding Instatic's Capability-Based Security Model
The foundation of Instatic's access control resides in the @core/capabilities package, which serves as the single source of truth for all 36 capability identifiers. Server-side code imports this canonical list and extends it with system role definitions in server/auth/capabilities.ts.
This design ensures that capability strings remain consistent across the frontend and backend, preventing drift between what the UI checks and what the API enforces.
The 36 Core Capabilities and System Roles
Capability Registry (@core/capabilities)
All possible permissions live in the core package as a master list. The server imports these via:
// server/auth/capabilities.ts
import { CORE_CAPABILITIES, type CoreCapability } from '@core/capabilities'
This import provides the complete set of 36 capabilities that any role can potentially hold.
System Role Definitions (server/auth/capabilities.ts)
Instatic defines four built-in system roles within the SYSTEM_ROLES array. Each role receives a specific subset of capabilities:
- Owner (
owner): Receives every capability via the spread operator (...CORE_CAPABILITIES). This role has full-system access and automatically inherits new capabilities on server boot. - Admin (
admin): Holds a hard-coded list includingdashboard.read,site.read,site.structure.edit,users.manage, and others. The Admin role cannot manage roles. - Client (
client): Limited to content editing capabilities such assite.content.edit,media.read, anddata.custom.tables.read. Cannot modify structure or styles. - Member (
member): Public-facing account with no administrative capabilities by default.
The FORCE_SYNC_ROLE_IDS array ensures that Owner and Admin roles automatically receive capability updates:
export const FORCE_SYNC_ROLE_IDS: readonly string[] = [OWNER_ROLE_ID, ADMIN_ROLE_ID]
Capability Helper Functions
server/auth/capabilities.ts exports utility functions for permission validation:
roleHasCapability(capabilities, capability): Checks whether a role's capability array contains a specific permission string.normalizeCapabilities(value): Sanitizes incoming capability lists by filtering out unknown strings and sorting them according to the master list, preventing injection attacks.
Authorization Guards and Runtime Checks
The Authz Module (server/auth/authz.ts)
Runtime enforcement occurs in server/auth/authz.ts, which exports guard functions that combine authentication and authorization checks.
requireCapability(req, db, capability) resolves the session cookie to an AuthUser, verifies the user possesses the required capability, and returns either the user object or a Response object indicating failure.
requireAnyCapability(req, db, capabilities[]) validates that the user holds at least one capability from the supplied list, useful for endpoints that accept multiple permission paths.
requireAuthenticatedUser(req, db) handles session resolution and MFA enforcement. If the user's account requires MFA but the session hasn't completed it, the function returns a 401 response with {error: 'mfa_required'}.
Step-Up Authentication with requireStepUp
Sensitive operations (user deletion, role management, device revocation) require fresh verification through step-up authentication. The requireStepUp(req, db, user) function checks the step_up_expires_at column on the session row:
export async function deleteUserHandler(req: Request, db: DbClient) {
const user = await requireCapability(req, db, 'users.manage')
if (user instanceof Response) return user
const stepUp = await requireStepUp(req, db, user)
if (stepUp) return stepUp
// Proceed with deletion...
}
If the step-up window expired, the function returns 401 {error: 'step_up_required'}, forcing the user to re-enter their password.
Multi-Factor Authentication (MFA) Implementation
TOTP Secret Generation and Verification
The server/auth/mfa.ts module handles Time-Based One-Time Password (TOTP) implementation:
generateTotpSecret(): Creates a cryptographically secure base-32 secret.totpProvisioningUri({issuer, accountName, secret}): Generates anotpauth://URL compatible with Google Authenticator and Authy for QR code generation.verifyTotpCode(secret, code, now?): Validates a 6-digit code against the current and adjacent time windows using constant-time comparison to prevent timing attacks.
Recovery Codes and Session Management
Instatic provides fallback authentication through recovery codes:
generateRecoveryCodes(): Creates a set of one-time-use backup codes.hashRecoveryCode(code): Returns a SHA-256 hash for secure storage.findMatchingRecoveryCodeHash(hashes, code): Performs constant-time lookup of recovery codes.
The session layer (server/auth/sessions.ts) tracks MFA requirements via sessionRequiresMfa and manages step-up expiry through getSessionStepUpExpiresAt, ensuring that elevated privileges time out appropriately.
Request Lifecycle: From Authentication to Authorization
Instatic processes every secured request through a layered pipeline:
- Session Resolution:
requireAuthenticatedUserextracts and hashes the session cookie, validating the user identity and checking MFA status. - Capability Verification:
requireCapabilityvalidates that the user's role includes the necessary permission from the 36-capability master list. - Step-Up Validation: For sensitive operations,
requireStepUpverifies the step-up window hasn't expired. - Response Handling: All guard functions return
Responseobjects on failure, allowing handlers to exit early with standardized error codes.
This orthogonal design keeps authentication, authorization, and MFA concerns separate, making each component independently testable.
Summary
- Instatic defines 36 fine-grained capabilities in
@core/capabilities, imported by the server to maintain a single source of truth. - Four system roles (Owner, Admin, Client, Member) receive capability assignments in
server/auth/capabilities.ts, with Owner and Admin force-synced on every boot. - Authorization guards (
requireCapability,requireAnyCapability) inserver/auth/authz.tsenforce runtime permissions and return standardized responses on failure. - Step-up authentication requires fresh password entry for sensitive operations, checked via
requireStepUpagainst the session'sstep_up_expires_attimestamp. - MFA implementation uses TOTP with
verifyTotpCodeand recovery codes with constant-time comparison functions inserver/auth/mfa.ts.
Frequently Asked Questions
How many capabilities does Instatic support?
Instatic supports 36 distinct capabilities defined in the @core/capabilities package. This list includes permissions for dashboard access, site structure editing, user management, media handling, and custom data operations. The master list ensures consistent permission naming across the entire application.
What is the difference between Owner and Admin roles?
The Owner role automatically receives all 36 capabilities via the spread operator (...CORE_CAPABILITIES), including role management permissions. The Admin role has a hard-coded capability list that excludes role management but includes user management and site structure editing. Only Owner and Admin are force-synced via FORCE_SYNC_ROLE_IDS to receive new capabilities automatically.
How does step-up authentication work?
Step-up authentication requires users to re-enter their password for sensitive operations. When requireStepUp is called, it checks the step_up_expires_at column in the session record. If the timestamp has passed, the function returns a 401 error with {error: 'step_up_required'}, forcing the client to recollect the password before proceeding with actions like user deletion or role changes.
What MFA methods does Instatic support?
Instatic supports TOTP (Time-Based One-Time Password) through authenticator apps like Google Authenticator and Authy, plus recovery codes for account recovery. The system uses generateTotpSecret for setup, verifyTotpCode for validation with constant-time comparison, and generateRecoveryCodes for backup access. MFA requirements are enforced at the session layer via sessionRequiresMfa.
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 →