You're reading docs for Nextly Alpha. APIs may change between releases.

Plugins

Plugins

Extend Nextly with official and custom plugins that add collections, admin views, hooks, and API endpoints.

Parts of this page need the release after 0.0.2-alpha.66: the typed, owner-checked db and db.raw (on 0.0.2-alpha.66 db is the Drizzle instance itself).

Alpha (0.x) — pin your versions.

The plugin SDK is alpha: its @public surface becomes semver-protected at 1.0 (see API stability). Build against @nextlyhq/plugin-sdk — the stability boundary — and until then pin exact nextly / @nextlyhq/plugin-sdk versions.

Plugins extend Nextly with new functionality without modifying core code. A plugin can add collections, register lifecycle hooks, inject custom admin views, and transform configuration -- all through a single plugins array in your config.

Official Plugins

PluginPackageDescription
Form Builder@nextlyhq/plugin-form-builderVisual drag-and-drop form builder with submission management, email notifications, spam protection, and export
Page Builder@nextlyhq/plugin-page-builderBlocks your team arranges on a page, rendered on the server with no client JavaScript by default
SEO@nextlyhq/plugin-seoAn SEO field group on the collections you name, and a sitemap of published content over plain HTTP. Turning those fields into <meta> tags is buildMetadata from nextly/runtime, which is core

Community Plugins

Community plugins are distributed on npm — search the nextly-plugin keyword. To get yours listed here, see Contributing a plugin.

No community plugins listed yet — be the first.

Adding a Plugin

Install the plugin package, then add it to your defineConfig call.

// nextly.config.ts
import { defineConfig } from "nextly";
import { formBuilder } from "@nextlyhq/plugin-form-builder";

const fb = formBuilder();

export default defineConfig({
  plugins: [fb.plugin],
  collections: [Posts, Users, Media],
});

Plugin collections are merged automatically -- you do not need to spread them into the collections array yourself. The plugin's config() transformer handles this during initialization.

For simpler setups where you do not need to customize plugin options, use the pre-configured default instance:

// nextly.config.ts
import { defineConfig } from "nextly";
import { formBuilderPlugin } from "@nextlyhq/plugin-form-builder";

export default defineConfig({
  plugins: [formBuilderPlugin],
  collections: [Posts, Users, Media],
});

How Plugins Work

Every plugin implements the PluginDefinition interface from @nextlyhq/plugin-sdk, the stable import surface for plugin authors. The lifecycle has two phases:

1. Declarative + setup phase

Plugins declare schema, permissions, routes, and admin UI in contributes (read by the host without running the plugin). Before services start, Nextly also runs each plugin's optional setup(config) escape hatch — all setups run before any init. It receives copies of the plain settings and the plugin definitions, without the database adapter, logger or any other live handle, and returns the config the boot continues with.

import { definePlugin } from "@nextlyhq/plugin-sdk";

export const myPlugin = definePlugin({
  name: "@acme/nextly-plugin-example",
  version: "1.0.0",
  nextly: ">=0.0.2-alpha.66",
  contributes: {
    collections: [MyCollection], // merged automatically by the pipeline
  },
  setup(config) {
    return { ...config }; // escape hatch for advanced config transforms
  },
});

2. Initialization Phase

After all services are registered, Nextly calls each plugin's init(ctx) with a PluginContext. It provides:

  • services -- managed, secure-by-default data access ({ as: 'system' } to elevate)
  • db -- typed, owner-checked access to your plugin's tables and its dependencies' tables (unmanaged); db.raw is the Drizzle builder
  • hooks / events / filters / actions -- in-transaction hooks, post-commit events, typed seams
  • self -- your entities' resolved slugs after any host .rename()
  • logger, config, nextlyVersion
import { definePlugin } from "@nextlyhq/plugin-sdk";

export const auditPlugin = definePlugin({
  name: "audit-log",
  version: "1.0.0",
  nextly: ">=0.0.2-alpha.66",

  async init(ctx) {
    ctx.hooks.on("afterCreate", "*", async context => {
      ctx.logger.info("Created entry", {
        collection: context.collection,
        id: context.data?.id,
      });
    });
  },
});

See the Plugin Author Guide for the full workflow.

Plugin Admin Integration

Plugins can control where their collections appear in the admin sidebar and customize their visual appearance.

import type { PluginDefinition } from "@nextlyhq/plugin-sdk";
import { MyCollection } from "./collections/my-collection";

const plugin: PluginDefinition = {
  name: "my-plugin",
  version: "0.1.0",
  nextly: ">=0.0.2-alpha.63",
  contributes: { collections: [MyCollection] },

  // Placement and appearance stay at the top level. This is complementary to
  // `contributes.admin`, which is the declarative menu, pages and views surface.
  admin: {
    order: 50,
    description: "What this plugin does",
    appearance: {
      icon: "BarChart",
      label: "Analytics",
      badge: "Beta",
      badgeVariant: "secondary",
    },
  },
};

Plugins can also register custom admin view components. The Form Builder plugin, for example, replaces the default collection edit view with a visual drag-and-drop builder. Components are referenced by path strings (e.g., @nextlyhq/plugin-form-builder/admin#FormBuilderView) and auto-registered when the admin panel loads.

Overriding Plugin Behavior

Host applications can override a plugin's sidebar placement and appearance through the admin.pluginOverrides section of defineConfig:

// nextly.config.ts
import { AdminPlacement } from "nextly";

export default defineConfig({
  plugins: [fb.plugin],
  admin: {
    pluginOverrides: {
      "@nextlyhq/plugin-form-builder": {
        placement: AdminPlacement.COLLECTIONS,
        order: 10,
        appearance: {
          label: "Contact Forms",
        },
      },
    },
  },
});

Next Steps

Build a plugin

Ship & maintain

Reference