Overview
The plugins array in plumix.config.ts is how you add an entry type, a route or an admin page to your site. It is not the whole registration surface. Core registers its own blocks, a theme’s descriptor declares templates, blocks, shortcodes and redirects statically, and the redirects key in plumix.config.ts contributes redirect rules with no plugin involved. Installing a published plugin is a package install and one entry in that array.
What a plugin is
Section titled “What a plugin is”A published plugin and the local plugin your site keeps for its own model are the same object. Both come from definePlugin, both carry an id and a setup function, and the site calls each one’s setup once at boot with the same registration context. Content Modelling covers writing one. This page covers installing one you did not write.
The registration context a plugin receives is the same one your own plugin receives, and it is wide. Entry types and taxonomies, meta boxes on entries, terms and users, blocks, marks, shortcodes and patterns, field types, settings groups and pages, RPC routers, REST resources, raw routes, rewrite rules, redirects, archive types, MCP tools, scheduled tasks, capabilities, admin pages, dashboard widgets, login links, lookup adapters, template deps, filters and actions all arrive through the same ctx. A package that registers one shortcode installs exactly like a package that registers six entry types.
Installing one changes two things in your repository and can change two more at run time. The package lands in package.json and the descriptor lands in the plugins array. Beyond that, a plugin that owns database tables needs a migration generated and applied before its first request, and a plugin that registers an entry type mints that type’s capabilities, so roles gain permissions nobody wrote by hand.
The plugins that ship
Section titled “The plugins that ship”These are all of them — every package published under the @plumix/plugin-* scope, ordered by what a new site reaches for first. A scaffolded project installs only the ones you ticked, and SEO and Feeds are the two that start ticked; a project scaffolded with none has none.
@plumix/plugin-blog
Section titled “@plumix/plugin-blog”Registers the post entry type with the category and tag taxonomies.
@plumix/plugin-pages
Section titled “@plumix/plugin-pages”Registers the hierarchical page entry type rooted at /.
@plumix/plugin-media
Section titled “@plumix/plugin-media”Registers a media entry type, a two-phase upload, and a media() field builder. The upload sends bytes straight to storage when the storage adapter implements presignPut, and routes them through the worker when it does not.
@plumix/plugin-menu
Section titled “@plumix/plugin-menu”Registers navigation menus and the named locations a theme renders them into.
@plumix/plugin-comments
Section titled “@plumix/plugin-comments”Adds threaded, moderated discussion to the entry types you nominate.
@plumix/plugin-forms
Section titled “@plumix/plugin-forms”Renders a form you declared in plumix.config.ts as a block, and stores what visitors send. The form is a value in your repository rather than a row, so it deploys with your code and reverts with git revert.
@plumix/plugin-audit-log
Section titled “@plumix/plugin-audit-log”Records an activity feed and adds an audit helper to the request context.
@plumix/plugin-seo
Section titled “@plumix/plugin-seo”Writes the head meta a public page needs: a description, a robots directive, the Open Graph set with an entry’s timestamps and byline, the Twitter card, and the resolved social image. Every tag is gap-filled, so a theme that sets one keeps it. It also serves /robots.txt and the paged sitemap, and owns the og:image resolution chain that @plumix/plugin-og contributes a card to.
@plumix/plugin-og
Section titled “@plumix/plugin-og”Renders a social card per page — an entry, a term or content-type archive, an author, a date, the front page — serves it from a route of its own, and carries it into the page’s og:image. The URL carries the card’s digest and is served immutable, so an edit publishes a link the scrapers have to fetch again. Its ogCards slot is a rule kind of the theme descriptor’s, resolved by the same walk templates uses — Custom Rule Kinds takes it apart as the worked example.
@plumix/plugin-feeds
Section titled “@plumix/plugin-feeds”Serves RSS 2.0 and Atom for the site, each entry type, each taxonomy term, each author and each date period, and advertises the matching one in every page’s head. Nothing serves /feed without it.
@plumix/plugin-search
Section titled “@plumix/plugin-search”Keeps a full-text index of everything the site publishes. Installing it materializes a plain-text projection of every searchable entry and an SQLite FTS5 index over it. Triggers in the database close both boundaries where the index could drift from the content, so a seed, a migration or a bulk import cannot leave a site searching stale text. An entry saved through the editor is indexed after the response, so nobody waits for it.
Quickstart
Section titled “Quickstart”Adding comments to the recipe site. comments owns a database table, which makes this the longest install path any of them takes.
Install the package with whichever package manager the project uses:
pnpm add @plumix/plugin-commentsnpm install @plumix/plugin-commentsyarn add @plumix/plugin-commentsbun add @plumix/plugin-commentsThen:
-
Put it in the
pluginsarray.commentsis a factory, so you call it. The site’s own plugin stays in the array beside it.import { auth, plumix } from "plumix";import { comments } from "@plumix/plugin-comments";import {cloudflare,cloudflareDeployOrigin,d1,} from "@plumix/runtime-cloudflare";import { recipes } from "./plugins/recipes";import { theme } from "./theme";export default plumix({runtime: cloudflare(),database: d1({ binding: "DB", session: "auto" }),auth: auth({passkey: {rpName: "Recipes",...cloudflareDeployOrigin({workerName: "recipes",accountSubdomain: "your-account",localOrigin: "http://localhost:5173",}),},}),plugins: [recipes, comments({ entryTypes: ["recipe"] })],theme,}); -
Generate and apply the migration.
plumix migrate generaterewrites.plumix/schema.tsfrom the resolved config, so it picks up the new table only once the descriptor is in the array.Terminal window pnpm plumix migrate generatepnpm plumix migrate apply --local -
Restart the dev server.
Terminal window pnpm devRecipes now accept comments, and the moderation queue is in the admin.
Two shapes of export
Section titled “Two shapes of export”A plugin package exports either a descriptor or a function that returns one, and which of the two decides whether you write pages or pages().
@plumix/plugin-pages takes no options, so it exports the descriptor itself.
import type { PlumixConfigInput } from "plumix";
import { pages } from "@plumix/plugin-pages";
export const plugins: PlumixConfigInput["plugins"] = [pages];The other seven export a factory instead, so you call it. Options are read at boot, either in the factory call itself or when the plugin’s setup runs, and nothing re-reads them after that. Changing an option means editing the call and restarting.
import type { PlumixConfigInput } from "plumix";
import { auditLog } from "@plumix/plugin-audit-log";import { blog } from "@plumix/plugin-blog";import { comments } from "@plumix/plugin-comments";import { feeds } from "@plumix/plugin-feeds";import { media } from "@plumix/plugin-media";import { menu } from "@plumix/plugin-menu";import { og } from "@plumix/plugin-og";
export const plugins: PlumixConfigInput["plugins"] = [ blog(), media(), menu({ locations: { primary: { label: "Primary navigation" } } }), comments({ entryTypes: ["recipe"], mode: "all", maxDepth: 2 }), auditLog(), og(), feeds(),];feeds() takes no options at all, and every option the others take has a default, so the empty parentheses on blog(), media(), auditLog(), og() and feeds() are a complete call. comments() on its own holds an address’s first comment for review and auto-approves once that address has an approved comment behind it. Its other defaults are three levels of reply nesting, twenty root comments to a page, and five submissions per source per ten minutes.
Install order
Section titled “Install order”The array is the order setup runs in. setup is not handed the registry, because there it would hold only what the plugins ahead of it had registered.
A descriptor’s optional afterSetup runs once every plugin’s setup has, with ctx.plugins holding everything they registered. It is where a plugin registers what it derives from other plugins’ types, such as a feed per entry type or an SEO box on every public taxonomy, and it sees the whole set wherever the plugin sits in the array. afterSetup functions run in array order too, so what one registers there is visible only to the afterSetup of a plugin listed after it.
One step runs earlier than that and does not care about order. A descriptor’s optional provides function runs for every plugin in the array before any plugin’s setup runs, so a helper published with extendPluginContext is on every plugin’s setup context whatever the order.
provides has a second call, extendAppContext, which puts a helper on the per-request context instead of the setup context. That is the one @plumix/plugin-audit-log uses for audit, so ctx.audit is there for any plugin’s routes, hooks and RPC procedures at request time, and no plugin’s setup can reach it.
Public URLs do not depend on array order either. The router compiles taxonomy rules ahead of entry-type rules and sorts every rule by a priority number, so a plugin whose entry type sits at the URL root cannot shadow another plugin’s routes by being listed first. Routing covers how a URL reaches the rule that serves it.
Plugins that own tables
Section titled “Plugins that own tables”@plumix/plugin-comments, @plumix/plugin-audit-log and @plumix/plugin-search each store rows in a table of their own, and each declares a schemaModule naming where that table’s definition lives. For @plumix/plugin-audit-log that is the default storage’s table; a swapped storage names its own, or none. plumix migrate generate reads the resolved config, writes .plumix/schema.ts re-exporting core’s schema plus every declared plugin schema, and hands the file to drizzle-kit. Skip the generate step and the emitted migration contains core’s tables only, so the plugin’s first query fails on a missing table.
The rest add no tables of their own. @plumix/plugin-feeds reads the shared entries table and writes nothing at all. @plumix/plugin-blog and @plumix/plugin-pages register entry types, and every entry of every type is a row in the shared entries table. @plumix/plugin-menu goes further and reuses the same tables for its own model: a menu is a row in terms, a menu item is a row in entries, and membership is a row in entry_term. @plumix/plugin-media stores an asset as an entry and the bytes in object storage, and @plumix/plugin-og stores a rendered card in the same bucket, keyed by its content.
Cloudflare Workers covers applying a migration to a deployed D1 database rather than the local one.
Schema drizzle cannot express
Section titled “Schema drizzle cannot express”drizzle-kit models tables, columns and indexes. A plugin needing DDL outside that — a full-text virtual table, a trigger — declares it as sqlMigrations instead: a name, and the statements to run.
import { definePlugin } from "plumix/plugin";
export const archive = definePlugin("archive", { setup: () => {}, sqlMigrations: [ { name: "fts_index", statements: [ "CREATE VIRTUAL TABLE `entry_fts` USING fts5(body, content='entry_search', content_rowid='id')", ], }, ],});plumix migrate generate emits each one as its own migration after the schema diff, so the DDL lands behind the tables it references, and records it in drizzle’s journal. wrangler d1 migrations apply orders by the number a filename starts with, and drizzle-kit numbers its next migration from the journal’s last entry — so a hand-written file dropped into drizzle/ gets the same number as the next generated one, and which of the two runs first is down to the rest of the filename.
One rule about the SQL itself: wrangler decides where a BEGIN … END block ends by looking for whitespace before the END, and appends its own bookkeeping statement to the file it runs. A trigger whose END sits directly on the previous statement’s semicolon would leave the block open and take that statement into the trigger body, so generation moves a trailing END onto its own line.
A name is the migration’s identity within its plugin, and a migration already in the journal is never emitted twice. Renaming one emits it again rather than editing what already shipped — the same rule that applies to any migration that has reached a database.
Plugins that ship admin UI
Section titled “Plugins that ship admin UI”Five add a page of their own: media has the library at /media, menu has the menu builder at /menus, comments has the moderation queue at /comments, audit-log has the activity feed at /audit-log, and forms has the submissions inbox at /form-submissions. Two add a surface without adding a page — seo contributes a page under Settings and a preview of how an entry will read in search results to the editor sidebar, and og contributes a preview of its social card to the same sidebar, but only when og({ preview }) asks for the box, since rendering it is the only reason that chunk exists. Blog, pages, feeds and search ship no admin UI at all.
Each of the seven declares a path to an admin entry module, and the Vite plugin compiles every declared entry into one bundle for your site during plumix build and plumix dev.
The consequence is that a newly installed admin-facing plugin appears after a build, not after a config reload. If the admin looks unchanged once the package is in the array, stop the dev server and start it again.
Two version tracks
Section titled “Two version tracks”The platform releases in lockstep. plumix and the five packages behind it, @plumix/core, @plumix/blocks, @plumix/admin, @plumix/admin-editor and @plumix/admin-ui, share one version number and go out together. You depend on plumix and get the rest transitively, so there is one platform number to pin. Your runtime adapter, @plumix/runtime-cloudflare, sits outside that group and carries its own number.
The eleven plugins each release on their own number, and those numbers have already diverged from each other and from the platform. One plugin sitting several minor versions ahead of another says nothing about either one’s compatibility with the platform or with each other.
Compatibility is carried by a peer dependency instead. Each plugin declares plumix as a peer with the range >=0.1.0 <1.0.0, so any pre-1.0 platform release satisfies every plugin, and upgrading plumix does not oblige you to upgrade the plugins beside it. A plugin republishes when its own code changes, not because the platform moved.
Scaffolding with plugins already wired
Section titled “Scaffolding with plugins already wired”create-plumix-app can do the install and the array entry for you. Each plugin package describes itself to the scaffolder, which splices the import, the registration and any config slot the plugin needs into the generated plumix.config.ts and wrangler.jsonc.
pnpm create plumix-app my-site --plugins blog,pagesPicking media this way is what puts storage: r2({ binding: "MEDIA" }) in the config and an r2_buckets entry in the Workers configuration. Installation walks the wizard, and Project Structure covers what it wrote.
Related
Section titled “Related”The plugins array is one slot among twenty in Configuration, which covers the file these snippets edit. Writing a descriptor rather than installing one is Content Modelling, and the registration options these plugins pass are documented in Entry Types. A plugin that contributes a field type, as the media plugin does, extends the roster in Field Types. A plugin can also contribute a per-request data dependency a template declares and receives, which the blog and comments plugins both do, and Themes covers the render layer that reads it.
Next steps
Section titled “Next steps”Read Blog and Pages next. Between them they cover the two entry types most sites install before anything else, and each is a worked example of the registration options this page only names. OG Cards renders a share image per published entry, and Feeds syndicates those entries as RSS and Atom — neither needs configuration beyond the array entry. Search replaces that page with ranked, full-text results, and unlike those two it needs a migration generated before its first request.
Translation Catalogs covers the descriptor’s i18n slot — the one that decides whether a plugin’s translations reach a site at all.
Nine pages in this section are not written yet, though everything they cover ships today. Menu documents navigation menus and the locations a theme renders them into. Comments documents threading, the moderation queue and the trust policy the quickstart above configured. Media documents the library, the upload flow and the media field types. Audit Log documents the activity feed. Publishing a Plugin covers taking a local plugin to npm, The Plugin Descriptor documents every slot on the descriptor object, Config Schema covers validating the options a factory takes, Admin Entry covers shipping admin screens, and Versioning and Peer Ranges goes further into the two tracks described above.