How Instatic Manages Plugin Permissions: A Deep Dive into the Declare-Then-Grant Model
Instatic enforces a declare-then-grant permission model where plugins declare required capabilities in their manifest, administrators approve a specific subset, and the runtime guards every privileged API call using assertPluginPermission checks against the stored grant set.
Instatic is an open-source CMS that implements a fine-grained permission system for its plugin architecture. Understanding how Instatic manages plugin permissions is essential for developers building secure extensions and administrators protecting site integrity. The system operates on a strict separation between what a plugin requests and what it is actually allowed to execute at runtime.
Permission Declaration in the Plugin Manifest
Every Instatic plugin begins by declaring its required capabilities in the plugin.json manifest file. This declaration defines the plugin's intended access scope but does not automatically grant those permissions.
Manifest Schema and Allowed Values
The permission schema is defined in [src/core/plugins/manifest.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts), which exports the PLUGIN_PERMISSION_VALUES constant containing all valid permission strings. Available capabilities include:
cms.routes- Register custom routescms.hooks- Access CMS lifecycle hookseditor.code- Modify editor functionalitynetwork.outbound- Make external HTTP requestsfrontend.assets- Serve static frontend assets
The manifest validation ensures that plugins can only request permissions from this predefined set, preventing typos or invalid capability requests.
User Consent and Grant Storage
When a plugin is installed, the admin UI surfaces the requested permissions for explicit approval. According to the source code in [src/core/plugins/runtime.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts), the runtime never trusts the manifest's declared list directly.
Instead, after administrator approval, the granted permission set is stored in the plugin record (cmsPluginRecords) and persisted as part of the plugin's metadata. This stored grant becomes the single source of truth for runtime permission checks, ensuring that plugins operate strictly within boundaries approved by the site owner.
Runtime Enforcement with PluginContext
Once granted permissions are persisted, the core runtime constructs a secure execution environment that injects only the approved capabilities into each plugin instance.
Building the Permission-Aware Context
The PluginRuntime class in [src/core/plugins/runtime.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) (lines 30-47) creates a PluginContext for each plugin containing api.plugin.permissions. This property is a snapshot of the granted permissions array specific to that plugin instance:
class PluginRuntime {
// ...
private emitPermissionChange(pluginId: string) {
const granted = this.pluginNames.get(pluginId)!.permissions
// `api.plugin.permissions` will contain exactly this `granted` array
}
}
Guard Enforcement via assertPluginPermission
Every privileged API in the SDK calls assertPluginPermission before executing sensitive operations. This guard function, exported from [src/core/plugin-sdk/guards.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/guards.ts), validates that the required permission exists in the context's permission set:
import { assertPluginPermission } from '@core/plugin-sdk'
export function registerWidget(api) {
assertPluginPermission(api, 'cms.routes') // throws if not granted
api.dashboard.registerWidget({
id: 'my-widget',
icon: 'star',
// …
})
}
If the permission is absent, the guard throws a descriptive error immediately, preventing unauthorized access before any privileged code executes.
Sandboxing and Lower-Level Checks
Certain permissions trigger additional enforcement layers beyond the guard checks, particularly for security-critical operations like network access and file serving.
Network Outbound Restrictions
Plugins with network.outbound permission do not receive unlimited internet access. The QuickJS sandbox's network.fetch bridge validates all target hostnames against an allow-list pattern defined in the manifest (NETWORK_HOST_PATTERN), as documented in [src/core/plugins/manifest.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) lines 53-56.
Frontend Asset Validation
For plugins exposing UI assets via frontend.assets, the runtime verifies that requested asset paths remain within the plugin's designated directory subtree (/uploads/plugins/{id}/{version}/). Path validation uses strict regex patterns defined in the manifest schema to prevent directory traversal attacks.
Admin UI and Permission Diff
The administrative interface includes a "Permission Review" component that helps site owners understand permission changes during plugin updates. Located in [src/admin/pages/plugins/components/PermissionReviewSection/computePermissionDiff.ts](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/plugins/components/PermissionReviewSection/computePermissionDiff.ts), this utility compares newly requested permissions against previously granted sets, highlighting additions and removals before the administrator confirms the update.
Summary
- Declare-then-grant model: Plugins declare intent in
plugin.json, but capabilities are separately granted and stored by administrators - Runtime isolation: Each plugin receives a
PluginContextcontaining only its approved permissions via [src/core/plugins/runtime.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) - Guard enforcement: The
assertPluginPermissionfunction validates every privileged API call against the stored grant - Layered security: Network and filesystem permissions include additional sandbox constraints beyond the manifest declaration
- Consent tracking: The permission diff UI ensures administrators explicitly review changes before granting new capabilities
Frequently Asked Questions
What happens if a plugin calls an API without the required permission?
The assertPluginPermission guard throws a runtime error immediately when a plugin attempts to access an API for which it lacks the granted permission. This prevents the plugin from executing any privileged logic and surfaces the violation to the developer or user immediately.
Where are granted permissions actually stored?
Granted permissions are stored in the cmsPluginRecords database table as part of each plugin's metadata record. The runtime in [src/core/plugins/runtime.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) retrieves these values when constructing the PluginContext, ensuring the runtime never relies on the manifest's declared list or client-side data.
Can a plugin request permissions dynamically after installation?
No. Instatic requires all permissions to be declared statically in the plugin.json manifest. While the permission set can be updated when a plugin version changes (triggering the diff review UI), plugins cannot request new permissions programmatically at runtime, maintaining strict security boundaries.
How does Instatic prevent plugins from accessing the wrong frontend assets?
The runtime validates asset paths against regex patterns that restrict access to the plugin's specific subdirectory under /uploads/plugins/{id}/{version}/. Additionally, the plugin must have the frontend.assets permission granted in its stored metadata, creating a two-layer validation system enforced in [src/core/plugins/manifest.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts).
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 →