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

Plugins

Plugin API Reference

The public plugin surface re-exported by @nextlyhq/plugin-sdk.

Alpha (0.x) — pin your versions. @nextlyhq/plugin-sdk is 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

ImportSurfaceReact?
@nextlyhq/plugin-sdkdefinePlugin, contributes/ctx/hook/event/filter types, PermissionSlug/EventName, secret()no
@nextlyhq/plugin-sdk/testingcreateTestNextlyno
@nextlyhq/plugin-sdk/clientuseCan, <Can>yes (optional peer)
@nextlyhq/plugin-sdk/adminregisterComponent, registerComponents, registerKnownPluginyes (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.events

Event-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.