Schema & data lifecycle
Contribute collections, extend existing entities (including Builder-made ones), relate across the merged schema, and clean up safely on uninstall.
A plugin's schema contributions flow through the same unified merge pipeline as your app's own code-first schema and the Visual Builder's schema (D3). The host can read them without running your plugin, so the admin and codegen can reason about them.
Contribute new entities
import { defineCollection, text, select } from "nextly";
const Redirects = defineCollection({
name: "redirects",
fields: {
fromPath: text({ required: true }),
toPath: text({ required: true }),
type: select({ options: ["301", "302"], defaultValue: "301" }),
},
});
definePlugin({
// ...
contributes: {
collections: [Redirects], // also: singles, components
},
});CRUD permissions are auto-seeded per contributed collection/single. The collection is tagged with your plugin's provenance and locked from Builder editing (it's plugin-managed).
Extend existing entities
contributes.extend adds fields to an entity you don't own, by slug (D12):
contributes: {
extend: [
{ target: "posts", fields: { metaTitle: text(), metaDescription: text() } },
// target can be an array to extend several at once
],
}As of the Builder schema lane (P8), extend may target a Builder-made collection,
not just a code-first one — the extra fields materialize (a migration adds the columns)
and are present at runtime. The added fields are attributed to your plugin in provenance,
so they're cleaned up if your plugin is removed.
Extending an entity that doesn't exist in the merged schema is a fail-fast boot error — see the error reference.
Migrations & databases
A plugin doesn't write SQL or manage tables itself — it declares schema through
contributes, and the host materializes it through the normal Nextly migration workflow:
nextly migrate # generate + apply migrations for the merged schema (incl. plugins)When a consuming app adds your plugin (or you change your contributed schema) and runs
nextly migrate, the plugin's collections and extend fields are folded into the merged
schema and turned into migrations alongside the app's own. The same plugin works across
Nextly's supported databases (PostgreSQL, MySQL, SQLite) because it goes through the
managed pipeline rather than dialect-specific SQL — but SQLite doesn't enforce some
constraints that Postgres/MySQL do, so test against a real database too (see
Testing).
Schema changes are applied on
migrate, not silently at boot — first boot only creates missing tables; it never alters an existing one. Ship schema changes as migrations so existing installs pick them up.
Relate across the merged schema
A plugin relationship (relationship(...)) may target any slug in the merged schema
— core (users, media), another plugin's collection, an app collection, or a
Builder-made one (D15). Targets are validated at boot; an unknown relationTo fails
fast.
Reference your own slugs via ctx.self
Never hardcode your own collection slugs in init/handlers — an integrator can rename
them with plugin.rename({...}) (D54). Read the resolved slug from ctx.self:
const slug = ctx.self.collections.redirects; // resolved, post-rename
await ctx.services.collections.listEntries(slug, {}, { as: "system" });Provenance (D14)
Every schema entity carries a source: "code", "ui" (Builder), "built-in", or
"plugin:<name>". This is what makes uninstall safe — Nextly knows which tables, fields,
and permissions belong to which plugin, and never touches the user's own (code/ui)
data.
Uninstalling & data cleanup
When you remove a plugin from defineConfig({ plugins }), its contributed tables become
orphans — present in the database, but no longer claimed by any registered plugin.
Nextly does not auto-drop them (your data is never deleted behind your back). Use the
prune CLI to review and remove them:
nextly prune # dry run — lists orphaned plugin:* tables/columns/permissions
nextly prune --force # actually drops them (and their data + seeded permissions)prune only ever targets plugin:<name> provenance. Code-first (code) and
Builder (ui) entities are always retained — they're yours, not the plugin's. This
is why declaring everything through contributes (rather than hand-writing tables via
ctx.db) matters: it's what lets uninstall be clean and reversible.
Tip for plugin authors: anything you create through
contributes/ctx.servicesis provenance-tagged and pruned automatically. Anything you create by reaching intoctx.db(raw DDL, side tables) is not tracked and will be left behind on uninstall — document it, or avoid it.
See also: Data access · Testing.
Admin UI
Contribute admin menu items, pages, settings, and collection-view overrides from a plugin — and control where they appear in the sidebar.
Testing your plugin
Integration-test a plugin against a real Nextly instance with createTestNextly — and why you should add at least one real-database test.