# How Instatic Manages Plugin Permissions: A Deep Dive into the Declare-Then-Grant Model

> Instatic uses a declare-then-grant model for plugin permissions. Plugins declare needs, admins approve, and Instatic guards API calls. Learn how Instatic manages plugin permissions.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-02

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/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)](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 routes
- `cms.hooks` - Access CMS lifecycle hooks  
- `editor.code` - Modify editor functionality
- `network.outbound` - Make external HTTP requests
- `frontend.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)](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)](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:

```typescript
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)](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:

```typescript
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)](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)](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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json), but capabilities are separately granted and stored by administrators
- **Runtime isolation**: Each plugin receives a `PluginContext` containing only its approved permissions via [[`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts)
- **Guard enforcement**: The [`assertPluginPermission`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/guards.ts) function 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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/guards.ts) 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)](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`](https://github.com/CoreBunch/Instatic/blob/main/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)](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts).