All concepts
15 — Concept

Plugins

A capability is a git repository copied into your app, not an npm package — it lands as ordinary source, reaches the app through five lines, and deleting the folder uninstalls it.

A repository copied in, never a package installed

A plugin here is a git repository whose files are copied into your application — exactly the way you got the kit itself. It is not an npm dependency, and that is the whole premise rather than an implementation detail.

A published package would mean a compiled surface, somebody else's build, and a version of this kit's internals frozen inside it. Copying means the plugin's code arrives as ordinary source in your repository: readable, debuggable, editable, and translated into your vocabulary on the way in — if you renamed the kit to your own name, the plugin arrives already wearing it. The price is that pulling in a later version is a patch rather than a version bump, and the kit already owns that machine: it is the same one that ports later releases of the kit into a copy.

Installing a plugin: the tool reads the plugin's repository, prints a plan, and only copies files into your app once you agree The kit Plugin repo Your app contracts to build on plugin add <repo> manifest, then a plan read it, then --apply source, renamed on the way in generate the migration later kit releases skip it
Nothing is written until you have read the plan and re-run the command with --apply. The migration is generated afterwards, by your app, from your schema.
  1. The plugin is written against the kit's own contracts, in the kit's vocabulary, in its own repository.
  2. In your app you run pnpm plugin add <repo> — or a local path, which is how you author one.
  3. The tool reads that repository's manifest and prints a plan: what it is, the requirements it checks, the files, the five lines each barrel gains, the dependencies, and the steps left to you. Then it stops.
  4. You read it. Installing a plugin gives it full access to your Worker and your database, so this is a decision, not a download. On --apply it proceeds.
  5. The source is copied in, renamed into your app's vocabulary, and recorded.
  6. You generate the migration for its tables yourself, read the SQL, and run it.
  7. From then on a later kit release never touches a file the plugin owns — it has its own repository and its own release chain.

Why first-party only, for now. A plugin runs inside your Worker with your database connection. There is no sandbox that would make that otherwise, so installing one is as trusting as merging a pull request — and the honest answer is to say so, print the plan every time, and keep the set to code the kit's own authors wrote, rather than to pretend a review process exists.

Five lines are the whole seam

A plugin reaches the application through five barrel files and nothing else. Installing one writes those lines; removing one deletes them. Nothing else in the kit ever names a plugin, which is exactly what makes both directions reversible and reviewable.

The barrelWhat it carries
Shared contractsthe plugin's schemas, job types, agent and prompt keys, permission subjects and feature flags — read by the composers that build each of those registries
Serverits routes, job handlers, agents, agent tools, scheduled tasks, permission grants and post-sign-up hooks
Database schemaone line re-exporting its tables, so the migration tool and the isolation tests see them exactly as they see the kit's own
UIits pages, navigation entries, settings tabs and query keys
CLIits commands, added after the kit's own

Every closed list in the kit opens the same way: what was a literal becomes the core list, and the public name becomes the core list plus whatever the plugins contribute. Nothing that reads a registry moves — what moves is where you add to it.

The types do more than describe the shape. A plugin that declares three job types and writes two handlers does not compile — the handler table is checked for completeness against the keys that same plugin declared. A missing handler is an error in the plugin rather than a dispatch failure in the application that installed it.

A plugin that talks to the outside world gets four more things from the kit, and none of them is specific to anyone: a public route under /api/hooks/<plugin id> for a callback or a webhook, with no session behind it and a test that refuses one anywhere else; a signed, expiring token for a round trip through somebody else's site; a check of feature flags from a job or a schedule; and knowledge ingested by its upstream id, so syncing the same item again updates it instead of adding a copy. The connectors plugin is the first to use all four.

Four published entries, and an id that is a namespace

A plugin has exactly four entry files — one for the server half, one for the browser half, one for the shared contracts and one for the command line. Everything else inside it is private. That is what lets its version number promise something: if the kit could reach into a plugin's internals, or one plugin into another's, the number would cover a surface nobody could enumerate. A deep import across that boundary is a failing test, not a code-review preference.

The other half of living together is namespacing, and the plugin's id does all of it: its tables, its job types, its API prefix, its command, its feature and prompt and agent keys, its query keys and its own streaming events all carry the id. Two plugins in one application therefore cannot collide by construction rather than by luck.

Skills travel with the plugin

A plugin can also bring agent skills — instructions your coding agent follows to set up, run and repair what the plugin adds. It declares them in its manifest and keeps each in a skills/ directory; installing copies each one to .claude/skills/<name>/, upgrading replaces it, and uninstalling removes it. Skill names are namespaced by the plugin's id like everything else, a name that collides with the kit's own skills or another plugin's is refused, and the check fails a declared skill that is missing, misnamed or has no description. The connectors and analytics plugins ship the first ones.

What the tool will not do for you

Three refusals, and each of them is a bug it is declining to cause:

  • It never ships a migration. Your application generates it, once the schema line exists, and you read the SQL before it runs. A migration snapshot describes a whole cumulative schema, so importing a foreign one would teach the migration tool a current state that has never heard of your own tables — and its next suggestion would be to drop them.
  • It never edits your deployment files or writes a resource id. A storage bucket, a queue or a scheduled task the plugin declares becomes a numbered step in the plan: one provisioning command per environment, which creates the resource and writes the block into both files. A binding of a kind the kit cannot provision is refused at install time, naming it — rather than surfacing as an error after a deploy that quietly skipped it.
  • It never renames anything across releases. Add a column, migrate, drop the old one — because the migration tool's rename prompt has no non-interactive answer, and an unattended install would stop dead on it.

Uninstalling is the mirror image: the directories go, the five lines go, and generating the next migration emits the DROP TABLEs. Tables left behind by a plugin that is no longer there are not a stable state, so dropping is the default and archiving them into a separate schema is the flag you ask for.

A plugin tests its behaviour; the app tests that it is a well-formed plugin

A plugin's own tests live inside it and run in your test suite. What the application adds is the set of checks no plugin author could make for themselves, because they are about the combination that one application happens to have installed: that every id really is a namespace, that nothing reaches past the four published entries, that a plugin's tables carry the same tenant isolation policy the kit's own tables do, and that the file loaded for every reader — including the ones who never open the plugin — imports nothing heavy and reaches every page lazily.

Compatibility is proved from both ends. A plugin declares the range of kit versions it supports; the kit's own pipeline runs its whole test gate a second time with the default plugins installed, and a plugin repository can call a workflow the kit ships to run itself against the oldest and the newest kit in its declared range — both ends rather than a midpoint, because those are the two that matter: the floor somebody is still on, and the ceiling the kit has just reached.

Two constraints that were measured, not assumed

A plugin's shared contracts must not import the registries that read them. Those registries read the plugin barrel, so importing one back closes a cycle — and two schema modules in a cycle do not fail to compile. They crash when the module is evaluated, with one side holding nothing. A type-only import is fine, because it is erased; a value import is not, and a test checks all three ways of spelling it.

A plugin declares database relations for its own tables only. Adding a second set of relations for one of the kit's tables merges at runtime but not in the type system: the two are intersected, and the result is that the table quietly loses the ability to load its related rows, everywhere in the application. The plugin's own side of the foreign key expresses the relationship completely; only the reverse direction is unavailable.

The commands

CommandWhat it does
pnpm plugin addInstall one, from a repository or a local path. Prints the plan and stops until --apply
pnpm plugin upgradePort the plugin's own later releases in, reading its release notes, exactly as a kit upgrade does
pnpm plugin removeDelete its directories, its five lines and its record; --archive keeps its data first
pnpm plugin listWhat is installed: id, version, where it came from, when
pnpm plugin checkAudit the installed set — one line per problem, and a non-zero exit if there is one
pnpm plugin exportWrite a plugin back out as a repository, which is how you start authoring one

What is installed is recorded in the same file that records where your copy of the kit came from, as one more replaceable surface — so an upgrade of the kit already knows never to touch a file a plugin owns, and an upgrade that would leave a plugin outside its supported range stops and says which one. A plugin installed into the kit itself, or with --local, is recorded in a git-ignored file instead: that is an authoring convenience, and committing it would push wiring for files a copy does not have into every copy made afterwards.

What exists today, and what moves next

The seam and the tooling land in the kit's 0.5.0 release, together with one plugin: example-feature, which exists to be deleted. It was the kit's demonstration feature flag and is now a plugin, grown until it exercises every slot — a tenant-scoped table, a gated set of routes, a background job, a permission subject, an agent tool, both post-sign-up hooks, a page with a navigation entry, and a command-line command — with its own tests pinning that one organisation cannot see another's rows in it. Proving schema and isolation on something removable comes before moving anything bigger.

Since then the kit has got smaller and the set has grown. Analytics moved out first, in 0.6.0 — the most self-contained subsystem, carrying the largest dependency in the application, and one plenty of apps never want — and it is still installed by default, so a fresh copy has it. Web knowledge, connectors and the Microsoft 365 provider were written as plugins from the start and are added only when you want them. Chat, agents and knowledge remain core, and every copy has them.

There is no third-party trust model — no sandbox, no review, no signature. "First-party only" is the whole of it, and it is the honest description of what a plugin can do once installed.