Plugin Authoring
A system-core plugin is a TypeScript package that declares an AppCapability and registers a backend via installCapabilityPlugin(). The UI layer generates admin pages automatically from the capability's crudResources — no Vue files required in the plugin package.
Package layout
packages/plugin-<name>/
src/
backend/
<resource>/
api.ts
prisma.ts
index.ts
i18n/
en.json
fr.json
types/
index.ts
index.ts
package.json
tsup.config.tsCapability contract
import type { AppCapability } from '@system-core/core/platform'
export const capability: AppCapability = {
id: 'plugin-<name>',
label: 'My Plugin',
version: '0.1.0',
// CMS content types this plugin manages
prismaModels: ['MyModel'],
// API routes the backend mounts
apiRoutes: [{ path: '/admin/<name>', methods: ['GET', 'POST', 'PUT', 'DELETE'] }],
// CRUD resource schemas — the UI generates list + edit pages from these
crudResources: [
{
resource: '<name>',
label: 'Items',
labelSingular: 'Item',
apiPath: '/admin/<name>',
fields: [
{ key: 'title', label: 'Title', type: 'text', required: true },
{ key: 'body', label: 'Body', type: 'richtext' },
{ key: 'status', label: 'Status', type: 'select',
options: ['draft', 'published'] },
{ key: 'publishAt', label: 'Publish', type: 'datetime' }
],
defaultSortField: 'title',
defaultSortDirection: 'asc',
softDelete: true
}
],
// Admin nav entries
adminNav: {
groups: [
{
label: 'My Plugin',
icon: 'folder',
items: [{ to: '/admin/cms/<name>', label: 'Items', icon: 'list' }]
}
]
},
// Admin pages (auto-resolved from crudResources — only override when custom)
adminPages: []
}Field types
AppCapabilityCrudFieldType supports:
| Type | Use |
|---|---|
text | Short single-line string |
textarea | Multi-line plain text |
richtext | HTML WYSIWYG |
markdown | Markdown editor |
number | Numeric input |
select | Single-value dropdown |
multiselect | Multi-value dropdown |
toggle / boolean | Checkbox |
date / time / datetime / daterange | Date pickers |
media | Media picker (uploads) |
file | File attachment |
color | Colour picker |
slug | Auto-slug from another field |
relation | Related record picker |
json | Raw JSON editor |
array | Repeatable sub-fields |
Backend registration
import { installCapabilityPlugin } from '@system-core/core/platform'
import { capability } from './index'
import en from './i18n/en.json'
import fr from './i18n/fr.json'
export const backend = installCapabilityPlugin(capability, {
version: capability.version,
order: 100,
// Seed UI strings on first install — idempotent
seedTranslations: { en, fr },
extension: ctx => ({
createDeps(prisma) {
return {
items: createItemDeps(prisma, ctx)
}
}
})
})UI strings (i18n)
Each plugin ships locale JSON files under src/i18n/. Keys follow the pattern <plugin>.<surface>.<label>:
{
"events.list.title": "Events",
"events.list.empty": "No events yet",
"events.action.new": "New Event",
"events.field.startDate": "Start date"
}Strings are written to the database on first install (namespace: 'ui:plugin-<capability-id>'). They are never overwritten once admin edits exist.
At runtime they are available via system.uiStrings and the public config endpoint.
API path convention
Admin API routes use the /admin/<resource> prefix — no leading /api:
// Correct
apiPath: '/admin/events'
// Incorrect — do not use /api prefix
apiPath: '/api/admin/events'Peer plugin dependencies
Declare peer capability requirements using peerCapabilities:
export const capability: AppCapability = {
id: 'plugin-foo',
peerCapabilities: ['plugin-bar'],
// ...
}installCapabilityPlugin validates peer presence at install time.
Full example
See packages/plugin-blog and packages/plugin-events in the repository for complete working examples.