Plugin Architecture
Comprehensive guide to the SveltyCMS Enterprise Plugin System — slots, hooks, migrations, sandbox, headless delivery, and config-page pattern.
On this page
SveltyCMS features a robust, enterprise-grade plugin architecture for deep customization without modifying the core. Plugins leverage Slots for UI injection, Lifecycle Hooks for CRUD interception, Migrations for database provisioning, a Sandbox for security isolation, and Headless Delivery boundaries for decoupled frontends.
System Overview
System Discovery & Autoloading
SveltyCMS dynamically scans and registers plugins on system boot. There are no static hardcoded imports of plugins in the core codebase.
- Vite Dev/Build: Scans all
src/plugins/*/index.tspaths using Vite’s native eager glob toolimport.meta.glob("./*/index.ts", { eager: true }). - Bun/Node runtime (CLI/Vitest): Utilizes a resilient directory scanning fallback (
fs.readdirSync) to import plugins dynamically.
This allows any custom or optional plugin to be added to or deleted from the src/plugins/ directory without causing compile-time errors.
Plugin Structure
Every plugin lives in src/plugins/{plugin-id}/ and follows this structure:
src/plugins/my-plugin/
├── index.ts ← Plugin definition + metadata
├── index.server.ts ← Server-only: hooks, migrations (optional)
├── components/ ← Svelte components (optional)
│ └── MyComponent.svelte
├── server/ ← Server-side services (optional)
│ └── service.ts
└── my-plugin.mdx ← Plugin documentation
Plugin Interface
export interface Plugin {
metadata: PluginMetadata; // Required: name, version, icon, category
config?: PluginConfig; // Optional: public/private settings schema
hooks?: PluginLifecycleHooks; // Optional: CRUD interception
ssrHook?: PluginSSRHook; // Optional: server-side data enrichment
ui?: PluginUIContribution; // Optional: UI columns, actions, edit tabs, slots
migrations?: PluginMigration[]; // Optional: database table provisioning
enabledCollections?: string[]; // Optional: restrict to specific collections
}
Injection Zones
Plugins inject UI components into predefined zones. All zones are rendered via the <Slot> component (src/components/system/slot.svelte).
| Zone | Location | Used By |
|---|---|---|
dashboard |
Main dashboard widgets | PageSpeed, Stripe |
sidebar |
Admin sidebar navigation | Editable Website |
entry_edit |
Entry editor tabs/panels | Any content plugin |
entry_edit_sidebar |
Entry editor sidebar | SEO plugins |
entry_edit_header |
Entry editor header actions | Workflow actions |
config |
System configuration area | Stripe, settings plugins |
config_grid |
Config page icon grid | Smart Importer, any config plugin |
collection_builder |
Collection builder page | Schema plugins |
| media_gallery | Media gallery page | Media plugins |
| media_gallery_toolbar | Media gallery toolbar | Image optimization plugins |
| user_profile | User profile/settings page | Profile extensions |
| user_profile_sidebar | User profile sidebar | Account plugins |
| entry_list_actions | Entry list action buttons | PageSpeed refresh |
| global-toolbar | Top toolbar (all routes) | System status (rendered in layout) |
| global-footer | Footer (all routes) | Debug bar (rendered in layout) |
| sticky-action-bar | Bottom sticky bar | Bulk operations |
Adding slots to a route page:
<script lang="ts">
import Slot from "@src/components/system/slot.svelte";
</script>
<Slot name="user_profile" />
<Slot name="collection_builder" />
Lifecycle Hooks
hooks: {
beforeSave: async (context, collection, data) => { return data; },
afterSave: async (context, collection, result) => {},
beforeDelete: async (context, collection, id) => {},
afterDelete: async (context, collection, id) => {},
// Auth hooks — fires after credential verification, before session cookie
afterAuthenticate: async (event) => {
// event.user, event.method, event.ip, event.userAgent
// return { deny: true, message: "Blocked" } to block login
// return { requires2FA: true } to force 2FA gating
},
}
Sandbox Boundaries
All plugin hooks execute within a sandbox (src/plugins/sandbox.ts) with architecture-aware dynamic scaling:
| Boundary | Limit | Why |
|---|---|---|
| Collection access | Only plugin_{id}_* for writes |
Prevent data corruption |
| Protected collections | users, sessions, tokens, roles, audit_logs blocked |
Never access auth data |
| Query count | 100 queries per hook (default); dynamically scaled up for migrations | Prevent resource abuse |
| Timeout | 5 seconds per hook (default); 30s for migrations | Prevent hangs |
| Error boundary | Catches all errors | Plugin crash ≠ CMS crash |
Dynamic Scaling: The sandbox adapts limits based on the execution context. Migrations receive a higher query budget (up to 500 queries) and extended timeout (30s) since they provision infrastructure. Hooks retain the strict 100-query / 5s limits to prevent runtime performance degradation. This architecture-aware approach ensures migrations can complete complex schema provisioning while hooks remain fast and safe.
Headless Delivery Boundaries
SveltyCMS plugins support decoupled frontend architectures through three key headless mechanisms:
1. Virtual UI Injection
Plugins can register API-only layout definitions for consumption by external frontends. Instead of injecting Svelte components into the admin UI, plugins export JSON-serializable slot contracts that external frontends (Next.js, Astro, mobile apps) render natively.
// Plugin exports virtual slot definitions for headless frontends
export const virtualSlots = [
{
id: "headless-blog-header",
zone: "page_header",
schema: { type: "object", properties: { title: "string", author: "string" } },
endpoint: "/api/plugins/blog/header-data",
},
];
2. Optional SvelteKit Site Starter
SveltyCMS ships a free, recommended SvelteKit public frontend at routes/(site) with Svedit inline page design. Website Starter is the default setup preset; SITE_STARTER_ENABLED (default true) toggles public routes at /.
- Free: Public routes,
pagescollection, native UI components - Not required: Disable for pure headless; use Astro/Next/Vue with the same APIs
See Site Starter guide.
3. Premium Live Preview (Editable Website plugin)
Real-time iframe preview and bidirectional editing require the Editable Website & Live Preview plugin (€14.99 marketplace, 14-day trial):
// Preview token flow (plugin calls this from the Live Preview tab)
POST /api/preview/authorize → { previewUrl: "https://myfrontend.com/?preview_token=..." }
- Collection schema:
livePreview: "/{slug}?lang={lang}"+plugins: ["editable-website"] - Works with the site starter (same origin) or external frontends (absolute URL pattern)
- Protocol:
svelty:init,svelty:update,svelty:field:click,svelty:save— see Live Preview Architecture
4. Stateless API Contracts
All plugin interaction with external frontends goes through documented REST/GraphQL endpoints with:
- Origin validation via plugin config (
allowedOrigins,frontendBaseUrl,allowedCheckoutOrigins) - Fail-closed semantics: If a plugin configures
failClosedOnCacheMiss: true, missing cache entries return 404 instead of falling through - Edge KV synchronization: Redirects and sitemaps push to edge stores for sub-10ms resolution on decoupled frontends
The Config Page + Plugin Pattern
The recommended architecture for admin-facing plugins uses TWO layers:
┌──────────────────────────────────────────┐
│ Config Page (routes/(app)/config/...) │ ← Admin UI
│ +page.svelte, +page.server.ts │ CRUD forms, list views
├──────────────────────────────────────────┤
│ Plugin (src/plugins/...) │ ← Business logic
│ hooks, migrations, services │ Auto-behaviors, cache sync
└──────────────────────────────────────────┘
Case Study: Smart AI-Driven Migration Pro
| Layer | File | Purpose |
|---|---|---|
| Config Page | config/migration/+page.svelte |
Drag-drop upload, format detection, progress UI |
| Config Page | config/migration/+page.server.ts |
Server actions: detect, dryRun, import, rollback |
| Plugin | smart-importer/index.ts |
Plugin metadata, config, slot registration |
| Plugin | smart-importer/index.server.ts |
7 AST compilers, 8 format parsers, UCP engine, DLQ |
| Plugin | smart-importer/components/ |
TransformationTree.svelte (visual schema mapper) |
Conditional tile: The Migration tile only appears in the Config grid when the plugin is installed and enabled. The config page’s +page.server.ts checks pluginRegistry.getPluginState('smart-importer', tenantId).
Case Study: Redirect Manager
| Layer | File | Purpose |
|---|---|---|
| Config Page | config/redirects/+page.svelte |
Manual CRUD for redirect rules |
| Plugin hooks | redirect-manager/index.server.ts |
Auto-redirect on slug change |
| Plugin migrations | redirect-manager/index.server.ts |
Creates redirects + redirects_mv tables |
| Plugin services | redirect-manager/index.server.ts |
Edge KV sync, Materialized View sync |
Available Plugins
| Plugin | Category | Docs |
|---|---|---|
| Smart AI-Driven Migration Pro | Migration | smart-importer.mdx |
| PageSpeed | Performance | pagespeed.mdx |
| Editable Website | Editing (premium) | editable-website.mdx — €14.99 live preview | | Redirect Manager | SEO | redirect-manager.mdx | | Sitemap | SEO | sitemap.mdx |
| WebMCP | AI | webmcp.mdx | | Cookie Consent | Privacy | cookie-consent.mdx | | Stripe | Payments | stripe.mdx | | Unified Data Hub | Data federation | unified-data-hub.mdx |
Best Practices
-
Lazy Loading: Use
() => import(...)for UI components to keep bundle size low. -
Type Safety: Import from
@src/plugins/typesfor all interfaces. -
Sandbox Compliance: Stay within 100 queries and 5s timeout per hook (migrations receive dynamic scaling).
-
Collection Prefixing: Use
plugin_{id}_for all plugin-owned collections. For abstract migrations, usedbAdapter.schema.ensureCollection()instead of rawcreateModel:// ✅ Recommended: abstract migration migrations: [ { up: async (dbAdapter) => { await dbAdapter.schema.ensureCollection("plugin_my-plugin_logs", { fields: [ { label: "Action", name: "action", type: "text", required: true }, { label: "Timestamp", name: "timestamp", type: "text" }, ], status: "publish", }); }, }, ]; // ❌ Legacy: raw createModel up: async (dbAdapter) => { await (dbAdapter as any).createModel({ _id: "plugin_my-plugin_logs" }); }; -
Optional Dependencies: Dynamic import server-side deps so plugins are truly optional.
-
Documentation: Every plugin must have an
.mdxfile in its directory. -
Config Page Pattern: Admin-facing plugins should pair with a config page for CRUD.
-
Conditional Tiles: Check
pluginRegistryin+page.server.tsto show/hide tiles.