Lifecycle, dependencies & order
How and when a plugin runs — the setup/init/destroy lifecycle, load order, declaring dependencies on other plugins, version compatibility, and enabling/disabling.
This page covers when a plugin's code runs, in what order, and how to express ordering and version constraints. For the errors these rules produce, see the error reference.
The lifecycle: contributes → setup → init → destroy (D4)
definePlugin({
name: "@acme/nextly-plugin-thing",
version: "1.0.0", // your plugin's semver (used by others' dependsOn ranges)
nextly: "^1", // core-compat range (see Version compatibility)
contributes: {
/* declarative nouns: collections, extend, permissions, routes, admin, events */
},
setup(config) {
// Escape-hatch config transform. ALL plugins' setup() run before ANY init().
// Don't mutate — spread and return a new object.
return { ...config };
},
async init(ctx) {
// Runtime wiring: subscribe to events, register hooks, read/write via ctx.services.
},
async destroy(ctx) {
// Teardown on shutdown / HMR / test teardown. Undo anything init() set up.
},
});Phase order:
contributesis read first (without running your plugin) — the host folds your schema, permissions, routes, and admin contributions into the merged config.setup(config)runs for every plugin before anyinit— so a plugin can adjust config that a later plugin'sinitrelies on.init(ctx)runs once services are live. This is where you do runtime work.destroy(ctx)runs on shutdown, hot-reload, and test teardown. Always clean up here — leaked subscriptions break HMR and tests.
Load order (D5)
Plugins are run in a topological sort over their declared dependencies, so a plugin
always initializes after the plugins it depends on. When two plugins have no
dependency relationship, their array order in defineConfig({ plugins: [...] })
breaks the tie.
export default defineConfig({
// a runs before b (array order), unless dependsOn says otherwise
plugins: [a(), b()],
});A dependency cycle is a fail-fast boot error (PLUGIN_RESOLUTION_ERROR,
reason: "dependency-cycle").
Dependencies (D5)
Declare dependencies on other plugins by package name → version range:
definePlugin({
name: "@acme/nextly-plugin-reports",
version: "1.0.0",
nextly: "^1",
// Hard requirement: must be installed and in-range, and inits first.
dependsOn: { "@acme/nextly-plugin-core": "^2" },
// Enhance-if-present: fine if absent; if present it must be in-range and inits first.
optionalDependsOn: { "@acme/nextly-plugin-search": "^1" },
});dependsOn— a missing dependency, or one whose installed version is outside the range, is a fail-fast boot error (reason: "missing-dependency"/"version-incompatible").optionalDependsOn— an absent optional dependency is fine; a present but out-of-range one fails fast (reason: "optional-version-incompatible"). Usectx.nextlyVersionor feature checks to branch on whether an optional dep is active.
If your plugin extends or relates to another plugin's entity, you must declare a
dependsOn on it — otherwise you get NEXTLY_SCHEMA_CROSS_PLUGIN_RELATION.
Version compatibility (D6)
Every plugin declares the core range it supports via nextly. It's checked at boot;
an out-of-range core is a fail-fast error (reason: "core-incompatible" /
"invalid-nextly-range").
nextly: "^1 || ^2", // may span multiple majorsPrereleases (alpha/beta) count as in-range. For runtime feature-detection, read
ctx.nextlyVersion rather than branching on the declared range.
Enabling & disabling (D49)
enabled defaults to true. Setting it to false skips the plugin's behavior
(init, hooks, events, routes, admin UI) but still applies its declarative schema —
so disabling a plugin doesn't drop its tables or strand its data. This lets an
integrator turn a plugin's behavior off without a destructive schema change.
definePlugin({ name: "@acme/nextly-plugin-thing", version: "1.0.0", nextly: "^1", enabled: false });To actually remove a plugin's tables and data, remove it from the config and run
nextly prune — see Schema & data lifecycle.
Inspecting
nextly plugins list # installed plugins + their contributions
nextly plugins info <name> # one plugin's details (resolution runs here too)See also: Author guide · Schema & data lifecycle · Error reference.
Auth extensibility
Extend authentication from a plugin — custom strategies (OAuth/SSO), auth-flow hooks, and first-class multi-step challenges (2FA) — without forking core.
Data access (ctx.services)
Read and write data from a plugin through the managed, secure-by-default service layer — queries, bulk operations, and system elevation.