Your first plugin
Add a feature plugin to the product monorepo, end to end.
This tutorial adds a minimal plugin to the product repo. Plugins are the
unit of feature composition — routes, services, tables, and UI slots all hang
off a Plugin object.
Prerequisites
- The dev environment set up.
- Read Product architecture first if the monorepo layout is new to you.
Steps
-
Create the package. Add
plugins/<name>/with apackage.json(@ktb/<name>,workspace:*deps) mirroring an existing small plugin such asplugins/notifications. -
Define the plugin object. In
src/index.ts, export plugin objects viadefinePluginfrom@ktb/kernel:export const myFeaturePlugin = definePlugin({ id: "my-feature", requiredDeps: [], // routes, services, tables, panel slots… });Registration is by convention —
packages/app-hostdiscovers everyplugins/*/src/index.tsthat exportsplugins. No central registry edit.If the plugin contributes UI (nav items, panel slots), also add
src/client.tsexporting apluginsarray with client-safe plugin objects — it must not import server code (no Effect services, db, orindex.tsinternals).@ktb/app-host/clientdiscovers it viaimport.meta.glob; parity betweenindex.tsandclient.tsis asserted byapps/web/src/plugins.test.ts. -
Add cross-plugin surface (optional). If other plugins need your data, export CQRS groups from
src/api.ts(Query.group/Commandfrom@effect-server-utils/cqrs) — never expose raw table reads. -
Add UI. Routes are TanStack file routes under
src/routes/; nav and panel contributions go through the plugin's extension points (panel.headerActions, etc.). -
Verify.
bun run checkat the root, thenbun run testscoped to your package (cd plugins/<name> && bunx vitest run).
Rules that will bite you
- Plugins cannot import from
apps/— composition happens inapps/web/src/routes/via injectedapiobjects. See Arch rules. - No
node:*/Bun/process/provider literals in plugin code. - Generated files (
routeTree.gen.ts, paraglide output, drizzle migrations) are regenerated, never hand-edited.