Plugin Development
A plugin is a TypeScript package that adds its own database tables, API routes and admin pages without Core containing plugin-specific business logic. Plugins are compiled into the API Worker at build time. There is no dynamic code loading. packages/plugin-hello is the minimal reference and packages/plugin-ecommerce is a full one.
For a step-by-step example, see Build a plugin.
Pieces
Section titled “Pieces”- Manifest:
id,name,version,sdkVersion(must match the SDK exactly, or the Worker fails at cold start), optionaldescription,dependencies,capabilitiesandpermissions. - Registration: the manifest, an admin Hono sub-app, an optional public sub-app, an optional config schema. Registered in
apps/api/src/plugins/registered-plugins.ts, the only Core file that imports a specific plugin. PluginContext: what an admin route handler receives:db,user,hasRole(),media,config,events,email,payments,accountsandlogger. Public routes get a smallerPluginPublicContextwith nouserorevents, but anidentityresolver for the shared session.requirePluginRole(minimum): route middleware using Core’s role hierarchy.manifest.permissionsis discovery metadata only, not checked at runtime.
Database
Section titled “Database”There is one D1 database and one migration history. A plugin’s tables are defined in packages/database/schema/plugins/<id>.ts, prefixed plugin_<id>_, and migrated with the normal pipeline:
pnpm --filter @kenresoft-cms/database generateCommit the migration before deploying with the plugin enabled. Only that plugin’s own repository code should query its tables. This is convention, not a mechanical boundary: PluginContext.db is the same Database type Core uses.
Configuration and secrets
Section titled “Configuration and secrets”Non-secret config lives in Core’s plugin_settings table through ctx.config.get() and .set(), validated by the plugin’s own Zod schema. Never store secrets there. Secrets are Worker secrets set with wrangler secret put.
Admin UI
Section titled “Admin UI”A plugin’s server code is in packages/plugin-<id>, but its admin nav entry and pages live in apps/admin/src/plugins/<id>/, registered in apps/admin/src/plugins/registry.ts. The split keeps apps/admin clonable on its own. Nav entries can be grouped, and disappear when the plugin is disabled.
Enablement
Section titled “Enablement”Bundled plugins are enabled by default and toggled live from Installed Plugins. A plugin’s public and admin routes are gated per request, so a disabled plugin returns 404 without a redeploy. Disabling does not delete data.
Events and limits
Section titled “Events and limits”ctx.events is an in-process, best-effort bus. Do not rely on it for anything that must not be lost. Use webhooks (database-backed, retried on the 5-minute cron) for durable delivery. Lifecycle hooks are declared but not executed. There is no per-capability context restriction.
The list of which plugins exist is configured separately in apps/api and apps/admin. Block-type contribution from plugins is not implemented.