# Add @suaveplan/api-plugin — plugin system foundation (1 of 5) **Package**: `@suaveplan/api-plugin` (NEW, universal-tier-server) **Status**: PROPOSED **Type**: new-package (split from upstream `@suaveplan/api`) - **Spec delta**: ./specs/api-plugin/spec.md - **Companions**: `add-api-tenant`, `add-api-openapi`, `add-api-migrations`, `enterprise-api` - **Position in chain**: foundation — gates all 4 companions ## Why The upstream `@suaveplan/api` package (8.7k LOC) bundles 7 architectural layers into one. The plugin system (`plugin/` + `hooks/` + topo-sort util — 2,034 LOC) is the foundation every other layer depends on. Splitting it out lets the tenant / openapi / migrations / app layers compose against a stable, fully-tested plug-and-play primitive without dragging the whole framework along. The upstream plugin system has known design issues that genesis fixes: dual-runtime hook execution (registry calls lifecycle hooks directly AND HookExecutor registers them — duplication hazard), unsafe `getService(name)` casts, unenforced `initTimeout` option, and asymmetric error policy between init (abort-on-throw) and shutdown (swallow-and-log). Forty-plus in-flight `enterprise-api-plugin-*` change folders are all blocked on this landing. ## What changes Net-new package at `0.1.0`. ADDED: - `definePlugin(descriptor): PluginDescriptor` — typed plugin factory. Descriptor includes `name`, `version`, `dependencies`, `optionalDependencies`, `priority?`, `enabledByDefault?`, `configSchema?: ZodType`, and lifecycle hooks (`onRegister/onInit/onReady/onClose`). - `PluginRegistry` class — single canonical registry. Methods: `register`, `registerAll`, `get`, `has`, `getAll`, `getSortedPlugins`, `initialize(context)`, `shutdown()`, `disable(name)`. Internally owns the topo-sort + cycle detection. - `PluginContext` — runtime injection: `{ logger: LoggerLike, eventBus: EventBusLike, getOwnConfig(): T, registerService(name: K, service: ServiceRegistry[K]): void, getService(name: K): ServiceRegistry[K] | undefined, hasService(name: string): boolean, signal: AbortSignal }`. - `ServiceRegistry` interface (empty by default) — designed for **module augmentation** so plugins declare their typed slots: `declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { db: DbAdapter } }`. - `topologicalSort(nodes): { sorted, unresolved }` — Kahn's algorithm + DFS cycle-detection diagnostic. Equal-in-degree nodes sorted alphabetically by id (deterministic). - `validateDependencies(nodes)` — single canonical validator (upstream has two; we ship one). - `HookExecutor` for **non-lifecycle hooks only** (request/db/custom plugin-contributed hooks). Lifecycle hooks (`onRegister/onInit/onReady/onClose`) are owned by `PluginRegistry` and called once per plugin — never via the executor. Eliminates upstream's dual-runtime hazard. - `PluginError extends SuaveplanError` with `defineCodes("PLUGIN", [...])` registry. Codes: `PLUGIN_NOT_FOUND`, `PLUGIN_ALREADY_REGISTERED`, `PLUGIN_INIT_FAILED`, `PLUGIN_DEP_MISSING`, `PLUGIN_CIRCULAR_DEPENDENCY`, `PLUGIN_INIT_TIMEOUT`, `PLUGIN_SHUTDOWN_FAILED`, `PLUGIN_INVALID_CONFIG`, `PLUGIN_HOOK_FAILED`, `PLUGIN_ABORTED`. - `PluginRegistryOptions` — `{ initTimeout?: number (default 30000), shutdownTimeout?: number (default 10000), strictOptionalDeps?: boolean, clock?: Clock }`. **`initTimeout` is enforced** — exceeded init rejects with `PluginError(PLUGIN_INIT_TIMEOUT)`. Same for shutdown. - `AbortSignal` propagation — every lifecycle hook receives `ctx.signal`; aborted ops reject with `PluginError(PLUGIN_ABORTED)`. - Subpath exports per feature module: `./define`, `./registry`, `./context`, `./service-registry`, `./hook-executor`, `./topo-sort`, `./error-codes`, `./types`. ## Out of scope - **Tenant lifecycle hooks** (`onTenantCreate/Update/Delete/Suspend/Activate`, `onPluginEnable/Disable`). Belong in `@suaveplan/api-tenant` (sibling change `add-api-tenant`) so non-tenant apps don't pay the surface cost. - **Request lifecycle hooks** (`onRequest/onResponse/onRequestError`). Belong in `@suaveplan/api` orchestrator + middleware layer (`enterprise-api`) because they need Hono request context to fire. - **Database migration auto-application.** The registry's upstream `applyMigrations()` between register/init is dropped. Migrations are owned by `@suaveplan/api-migrations` and run explicitly by the app orchestrator at the appropriate boot phase. - **OpenAPI registry integration.** `@suaveplan/api-openapi` consumes the plugin context but does not live inside the plugin package. - **Filesystem plugin discovery** (`PluginLoader.autoDiscover()`). Has two known bugs upstream (prefix mismatch between filter and rewrite) and adds a Node-only filesystem dep to what is otherwise a universal-tier package. Belongs in a follow-up `@suaveplan/api-plugin-loader` only if a real consumer asks. Explicit `plugins: [foo, bar]` arrays are the v1 contract. ## Source mapping | Capability | Upstream source | Notes | |---|---|---| | Plugin interface + descriptor | `../suaveplan/packages/server/api/src/plugin/plugin.interface.ts` L1-L201 | port shape; switch hook surface to 4 lifecycle hooks only (others move to sibling packages) | | `definePlugin` factory | `../suaveplan/packages/server/api/src/plugin/plugin-metadata.ts` L77-L123 | port shape; drop decorator helpers (require `reflect-metadata`) | | `PluginRegistry` class | `../suaveplan/packages/server/api/src/plugin/plugin-registry.ts` L44-L433 | port + enforce `initTimeout`, unify lifecycle runtime, switch errors to `defineCodes` registry, drop `applyMigrations` | | `PluginContext` | `../suaveplan/packages/server/api/src/plugin/plugin-context.ts` L43-L227 | port shape; `registerService`/`getService` become typed via `ServiceRegistry` module augmentation; drop `getPluginConfig(name)` cross-plugin coupling; ALS-based `getCurrentXxx()` helpers dropped (callers thread `ctx` explicitly) | | `topologicalSort` + `detectCycle` + `validateDependencies` | `../suaveplan/packages/server/api/src/utils/topological-sort.ts` L1-L157 | verbatim port + alphabetical tie-break for stable order; drop duplicate `validateDependencies` in registry | | `HookExecutor` (priority-ordered dispatch) | `../suaveplan/packages/server/api/src/hooks/hook-executor.ts` L1-L221 | port shape; **excludes the 4 lifecycle hook slots** (those are registry-owned); switch to `defineCodes` errors | | Plugin error subclasses | `../suaveplan/packages/server/api/src/errors/plugin-errors.ts` L1-L97 | replace with `defineCodes("PLUGIN", [...])` registry + `PluginError` discriminant | ## Impact ### Affected dependents None today — net-new on genesis. Forward-declared consumers (all sibling changes in this set + the 40+ in-flight `enterprise-api-plugin-*` packages): - `@suaveplan/api-tenant` (sibling) — adds tenant lifecycle hooks on top - `@suaveplan/api-openapi` (sibling) — registers routes via plugin `onInit` - `@suaveplan/api` (sibling) — composes the registry into `SuaveApp` - Every `@suaveplan/api-plugin-*` package (40+ in `openspec/changes/`) — implements `PluginDescriptor`