Plugin API Reference
The public plugin surface re-exported by @nextlyhq/plugin-sdk.
Alpha (
0.x) — pin your versions.@nextlyhq/plugin-sdkis the stability boundary — if it isn't exported there, it isn't public. Most of this surface is now@public(semver-protected); some is still@experimental. See API stability for the exact ledger.
Entry points
| Import | Surface | React? |
|---|---|---|
@nextlyhq/plugin-sdk | definePlugin, contributes/ctx/hook/event/filter types, PermissionSlug/EventName, secret() | no |
@nextlyhq/plugin-sdk/testing | createTestNextly | no |
@nextlyhq/plugin-sdk/client | useCan, <Can> | yes (optional peer) |
@nextlyhq/plugin-sdk/admin | registerComponent, registerComponents, registerKnownPlugin | yes (optional peer) |
definePlugin(def): PluginDefinition
interface PluginDefinition {
name: string;
version: string;
nextly: string; // core-compat range (boot-checked, may span majors)
dependsOn?: Record<string, string>; // required deps → version range
optionalDependsOn?: Record<string, string>; // enhance-if-present
enabled?: boolean; // default true; false = skip behavior, keep schema
contributes?: PluginContributions;
setup?: (config: NextlyConfig) => NextlyConfig; // all setups before any init
init?: (ctx: PluginContext) => void | Promise<void>;
destroy?: (ctx: PluginContext) => void | Promise<void>;
rename?: (map: Record<string, string>) => PluginDefinition; // integrator-side remap (D54)
}PluginContributions
interface PluginContributions {
collections?: CollectionConfig[];
singles?: SingleConfig[];
components?: ComponentConfig[];
extend?: Array<{ target: string | string[]; fields: FieldConfig[] }>;
permissions?: PluginPermission[]; // { action, resource, label?, description?, group? }
events?: Array<{ name: string }>;
routes?: PluginRoute[]; // { method, path, handler, requiredPermission?, public?, middleware? }
admin?: {
menu?: PluginMenuItem[]; // { label, to, icon?, order?, requiredPermission?, children? }
pages?: PluginAdminPage[]; // { path, component, requiredPermission? }
settings?: { component: ComponentPath };
views?: Record<string, PluginCollectionView>; // list/edit/before*/after* by collection slug
// widgets?: RESERVED (M8 / D58) — not rendered yet
};
}Component references are string paths: "<package>/<path>#<Export>", e.g. "@acme/x/admin#ReportsPage".
PluginContext (ctx)
interface PluginContext {
services: NextlyServices; // managed, secure-by-default; .collections supports { as: 'user' | 'system' }
db: DatabaseInstance; // raw Drizzle escape hatch (unmanaged)
hooks: PluginHookRegistry; // in-transaction, modify/abort
events: EventBus; // post-commit, observe-only, best-effort
filters: PluginFilterRegistry; // transform values on typed seams (D63)
actions: PluginActionRegistry; // ordered side-effects on typed seams (D63)
self: { name: string; collections: Record<string,string>; singles: Record<string,string> }; // resolved slugs (D54)
config: Readonly<NextlyConfig>;
logger: Logger;
nextlyVersion: string;
}Secure-by-default services (D35)
// Acts as the current user (RBAC enforced):
await ctx.services.collections.listEntries(slug, {}, { as: "user", user: ctx.user });
// Privileged work — explicit, visible elevation:
await ctx.services.collections.createEntry(slug, data, { as: "system" });Hooks & events
ctx.hooks.on(type, collectionSlug | "*", handler); // beforeCreate/afterCreate/… — can modify/abort
ctx.events.on(eventName, handler); // observe-only; e.g. `collection.${slug}.created`
ctx.events.emit(eventName, payload); // declare custom names in contributes.eventsEvent-name constants are exported: DocumentEvents, AuthEvents, MediaEvents.
Typed slugs (codegen)
CollectionSlug, SingleSlug, PermissionSlug, and EventName are string by default and narrow to the installed unions once nextly generate:types runs. Author code stays valid either way.
Client (/client)
useCan(permission: string): boolean;
const Can: React.FC<{ permission: string; fallback?: ReactNode; children: ReactNode }>;Admin registration (/admin)
registerComponent(path, Component);
registerComponents({ [path]: Component });
registerKnownPlugin(packagePrefix, async () => { /* register */ });When you run nextly generate:types, a plugin-admin-imports.generated.ts is emitted that calls registerComponents for you — import it once in the app and your contributes.admin components load with no manual wiring.
Testing (/testing)
createTestNextly(opts: { plugins: PluginDefinition[]; collections?: CollectionConfig[] }):
Promise<{ nextly; getService; hooks; events; adapter; destroy }>;Secrets
secret(value): Secret<T>; // auto-redacts in logs / JSON / inspect; .reveal() to read
isSecret(v): boolean;See the author guide for the full workflow and the error reference for boot-error meanings.