An installable Adonia plugin describes its server, browser, migration, route,
worker, doctor, and compatibility surfaces in one manifest. Ship the server
entry from . and, when descriptors use custom component types, the browser
entry from ./client at the same package version. Use vendor-prefixed
component keys such as acme/currency; adonia/* is reserved.
Define a manifest
definePlugin() preserves the manifest’s literal types. Panel.plugins()
performs the runtime checks: protocolVersion must equal Adonia’s current
protocol, version must be valid semver, and compat.adonia must include the
installed @adonia/core version.
import { fileURLToPath } from 'node:url'
import {
definePlugin,
PROTOCOL_VERSION,
type AdoniaPluginManifest,
} from '@adonia/core'
export const auditPlugin: AdoniaPluginManifest = definePlugin({
name: '@acme/adonia-audit',
version: '1.0.0',
protocolVersion: PROTOCOL_VERSION,
compat: { adonia: '>=0.0.1-alpha.0 <1.0.0' },
client: '@acme/adonia-audit/client',
migrations: [
fileURLToPath(new URL('./migrations', import.meta.url)),
],
})Migration entries are absolute directories, not individual files. Keep
the directory in the published package. Source packages commonly point at
./migrations; compiled packages then publish the corresponding
dist/migrations directory.
Attach manifests in order. Server hooks and runtime hooks preserve that order.
import { Panel } from '@adonia/core'
import { auditPlugin } from '#adonia/plugins/audit_plugin'
export default Panel.make('admin').plugins([auditPlugin])A protocol mismatch or a failing/invalid compatibility range throws
E_ADONIA_PLUGIN while the panel is registered, before routes or workers can
serve traffic.
Server hook
server(registry) runs once for every attached panel during provider boot.
The registry exposes the current panel and Adonis app, forwards ordered
hooks through registry.on(), and adds resources without replacing host
registrations.
import { definePlugin, PROTOCOL_VERSION } from '@adonia/core'
import AuditResource from '#adonia/resources/audit_resource'
export const auditPlugin = definePlugin({
name: '@acme/adonia-audit',
version: '1.0.0',
protocolVersion: PROTOCOL_VERSION,
server(registry) {
registry.resource(AuditResource)
registry.on('record:saved', ({ resourceClass, operation }) => {
console.info(resourceClass.slug, operation)
})
},
})The protocol-v1 events remain resource:booted, descriptor:compiling,
pipeline:building, record:saving, record:saved, record:deleting,
record:deleted, action:executing, action:executed, action:failed,
nav:building, and search:results. Import ADONIA_HOOK_EVENTS instead of
maintaining a second list.
pipeline.before(stage, fn), after(stage, fn), and replace(stage, fn) use
the addressable stages base, softDeleteScope, search, filters, sort,
and eagerLoad. A replacement must preserve panel, tenant, parent, lens, and
soft-delete isolation.
Server and route failures abort startup as E_ADONIA_PLUGIN. Ordinary
non-record hook failures are logged and isolated. Record hooks propagate
inside the write transaction so the transaction rolls back.
Legacy server plugins
The pre-manifest AdoniaPlugin shape remains source-compatible:
register(panel, app) still runs once per attached panel, boot(app) still
runs once per plugin name per process, and cacheKey still participates in
descriptor cache identity. New packages should use the manifest so tooling,
routes, workers, and compatibility are declared together.
Scoped routes
Manifest routes are created inside the protected panel route group. The callback cannot escape the panel’s domain, prefix, tenant middleware, or access gate. Each method takes a relative pattern, a handler, and a manifest-local name:
import type { HttpContext } from '@adonisjs/core/http'
import type { PluginRouteContext } from '@adonia/core'
const exportAudit = (ctx: HttpContext) => ctx.response.noContent()
export function routes({ router }: PluginRouteContext): void {
router.get('audit/export', exportAudit, 'export')
}The example is named
adonia.<panel>.plugin.@acme/adonia-audit.export. Leading slashes are
normalized to relative paths. Route callbacks are synchronous because Adonis
commits the route store immediately after provider boot.
Queue workers
workers maps reserved action names to ActionJobHandlers. Core dispatches
the matching handler before its normal action worker and supplies the
worker-process container, never an HTTP request:
import type { ActionJobHandler, ActionResult } from '@adonia/core'
const workers: Record<string, ActionJobHandler> = {
'acme.audit.export': async (job, { container }) => {
const worker = (await container.make('acme.auditWorker')) as {
work(input: unknown): Promise<ActionResult>
}
return worker.work(job)
},
}Worker names must be unique within a panel. The host still starts its queue consumer and binds application-specific credentials and services; attaching the manifest removes manual action-name routing.
Migrations and doctor checks
Devtools consumes manifests structurally, so @adonia/core does not import
the doctor runtime. A plugin configure hook can pass manifests to the
installer:
import { configureAdoniaApp } from '@adonia/devtools/configure'
import { auditPlugin } from '#adonia/plugins/audit_plugin'
const appRoot = new URL('./', import.meta.url).pathname
await configureAdoniaApp(appRoot, { plugins: [auditPlugin] })The installer copies every missing .ts, .js, .ts.stub, or .js.stub
migration with an exclusive timestamped filename and registers the
manifest’s doctor checks. A process that only needs diagnostics can call
registerPluginDoctorChecks([auditPlugin]) from
@adonia/devtools/doctor; repeated consumption of the same check object is
safe, while a different check with the same id is rejected.
Client half
The client string is package metadata; the browser bundle does not import
it automatically. Register the matching named UI plugin in the application’s
component registry:
import type { FieldProps, NamedAdoniaUiPlugin } from '@adonia/ui'
function AuditField({ node, control }: FieldProps) {
return (
<output aria-label={String(node.props.label ?? node.key)}>
{String(control.value ?? '')}
</output>
)
}
export const auditUiPlugin: NamedAdoniaUiPlugin = {
name: '@acme/adonia-audit',
version: '1.0.0',
protocolVersion: 1,
register(registry) {
registry.field('acme/audit', AuditField)
},
}import { adonia } from '@adonia/ui'
import { auditUiPlugin } from '#adonia/plugins/audit_ui'
export default adonia({
registry(registry) {
registry.use(auditUiPlugin)
},
})A missing client registration does not crash sibling rendering. Development
renders UnknownComponent; production renders nothing; both warn.
Package exports
Expose the manifest and advanced server primitives from ., and the named UI
plugin from ./client. Keep both versions equal. Before publishing, run the
plugin conformance kit from @adonia/core/testing; TypeScript alone does not
prove hook ordering, isolation, or transaction behavior.
See the generated API reference and the reference plugins for complete examples.