Introducing the MakerKit Plugin System: One Command to Add Features

MakerKit's new plugin system replaces git subtree with a single CLI command. Install feedback widgets, analytics, billing, and more in under 60 seconds.

If you've installed MakerKit plugins before, you know the drill: git subtree commands, merge conflicts, manual config wiring. It worked, but it wasn't fun.

Today that's over. We rewrote the entire plugin system from scratch. One command, under 60 seconds, fully wired:

npx @makerkit/cli@latest plugins add feedback

That's it. Files land in your monorepo, dependencies get installed, config files get wired up, environment variables get appended. No manual steps. No merge conflicts. Just review the diff, run your tests, and commit.

Adding a feature to your SaaS just became a 60-second operation.

This post is part of the CLI v2 release. For the full announcement including project update and the MCP server, see Introducing the MakerKit CLI v2.

This post covers what you need to know as a plugin user, then dives deep into the engineering behind it.

What changed (the short version)

  • Before: git subtree pull from a private repo, manually register the plugin in config files, add routes, set env vars. Pray there are no merge conflicts. Budget 30 minutes and hope nothing breaks.
  • After: npx @makerkit/cli@latest plugins add <plugin>. Done. The CLI handles file distribution, AST-based config wiring, and environment variable setup in a single step. What used to take half an hour now takes less than a minute.

How to install a MakerKit plugin:

  1. Run npx @makerkit/cli@latest plugins init to configure the registry
  2. Enter your GitHub username (the one tied to your MakerKit license)
  3. Run npx @makerkit/cli@latest plugins add <plugin-name>
  4. Review the changes with git diff
  5. Add required environment variables to your .env.local file
  6. Test and commit

The CLI requires a clean git working tree before making any changes. If something goes wrong, roll back with git checkout . && git clean -fd.

Available plugins

Thirteen plugins are available at launch, with more coming:

  • Feedback: Popup widget with admin dashboard for collecting user feedback
  • Waitlist: Signup form with admin approval workflow
  • Testimonial: Testimonial collection with video support and display widgets
  • Roadmap: Public roadmap with voting, comments, and kanban admin
  • Paddle: Paddle Billing integration (alternative to Stripe)
  • Google Analytics: GA4 tracking with configurable page view handling
  • PostHog: Product analytics with client and server-side event tracking
  • Umami: Privacy-focused, self-hosted analytics
  • SigNoz: OpenTelemetry-based application monitoring
  • Honeybadger: Error monitoring and uptime tracking with zero-config alerts
  • Directus: Headless CMS integration with content modeling and REST/GraphQL APIs
  • Meshes Analytics: Event tracking for user engagement and conversions
  • Supabase CMS: Use Supabase as a headless CMS with Markdoc rendering

All thirteen are available for the Next.js + Supabase variant. Next.js + Drizzle and Next.js + Prisma currently ship six plugins (Google Analytics, PostHog, Umami, SigNoz, Honeybadger, Directus), with the rest being ported.

Each plugin lands as a self-contained package under packages/plugins/ in your Turborepo monorepo. You own the code — read it, modify it, make it yours.

Getting started

If you're already a MakerKit customer, you can start using the new system right now. It takes about two minutes to set up.

First-time setup

npx @makerkit/cli@latest plugins init

This command does three things:

  1. Detects your project variant by reading package.json dependencies (Next.js + Supabase, Next.js + Drizzle, etc.)
  2. Prompts for your GitHub username, which is used to authenticate against our plugin registry
  3. Configures components.json with the MakerKit registry URL and your credentials

After init, your components.json will include:

{
"registries": {
"@makerkit": {
"url": "https://makerkit.dev/r/next-supabase",
"params": {
"username": "your-github-username"
}
}
}
}

Installing plugins

Install a single plugin:

npx @makerkit/cli@latest plugins add feedback

Or install several at once:

npx @makerkit/cli@latest plugins add umami posthog feedback

Running plugins add with no arguments shows an interactive multi-select list of every available plugin for your variant. Pick everything you need in one go.

The CLI walks through a strict sequence for each plugin:

  1. Verifies your git tree is clean (uncommitted changes = abort)
  2. Auto-initializes the registry if you haven't run init yet
  3. Validates the plugin exists and supports your variant
  4. Checks if it's already installed (by looking for the plugin directory on disk)
  5. Runs the codemod, which handles file distribution and config transforms
  6. Appends any required environment variables to .env.local and .env.example
  7. Prints a summary with next steps

Listing plugins

npx @makerkit/cli@latest plugins list

Shows every available plugin for your variant, with installation status detected from your filesystem. No manifest files to maintain.

Updating plugins

npx @makerkit/cli@latest plugins update

Or target specific plugins:

npx @makerkit/cli@latest plugins update umami feedback

If your local files differ from the registry version, the CLI lists the modified files and asks for confirmation before overwriting. Your customizations are never silently replaced.

Checking for updates and diffing

npx @makerkit/cli@latest plugins outdated

Shows which installed plugins have newer versions available. If you want to inspect exactly what changed before updating, run:

npx @makerkit/cli plugins diff umami

This shows a colored unified diff (via git diff) between your local files and the latest registry version. Running plugins diff with no arguments prompts you to select an installed plugin.

Full command reference

CommandDescription
plugins listList available and installed plugins
plugins add [id...]Install one or more plugins
plugins update [id...]Update installed plugins to the latest version
plugins outdatedCheck which installed plugins have updates
plugins diff [id]Show a git-style diff against the latest version
plugins initSet up your GitHub username for registry access

Architecture deep dive

Now for the fun part. Making "one command, zero effort" actually work required some real engineering. If you're curious about the decisions behind this system, or you're building something similar for your own product, read on.

The two-layer problem

Installing a plugin into a Turborepo monorepo requires solving two distinct problems:

Problem 1: File distribution. A plugin is a set of files (components, server actions, schemas, migrations, config) that need to land in specific locations across the monorepo. Some go into packages/plugins/feedback/, others might target apps/web/app/api/feedback/. We also need to install the right npm dependencies.

Problem 2: Config wiring. After the files are in place, the plugin needs to be integrated. That means adding imports to config files, registering providers, inserting navigation entries, and updating middleware. This is the part that used to require manual work and frequently went wrong.

No single tool solves both problems well. So we built two layers:

LayerToolResponsibility
File distributionshadcn Registry format + custom orchestratorFetch registry JSON, write files to target paths, install npm deps
Config wiringCodemod.com (JSSG)AST transforms on existing project files

The MakerKit CLI orchestrates both layers in sequence. One command, two engines.

Layer 1: The Registry

We started with shadcn/ui's registry protocol. Their format for describing files and dependencies is well-designed, and shadcn build makes it easy to compile plugin source into distributable JSON. We use it on the build side.

On the install side, we hit a wall: shadcn CLI doesn't support nested file paths. It was designed for flat component directories, not a monorepo plugin that spans packages/plugins/feedback/src/components/, packages/plugins/feedback/src/lib/server/, and packages/plugins/feedback/migrations/. When your plugin has 15+ files across deeply nested directories, shadcn add just doesn't work.

So we built our own file download orchestrator. It fetches the registry JSON directly from our API, reads the file entries, and writes each one to the correct target path. Same format, custom downloader.

Each plugin is described as a "registry block" with this structure:

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "feedback",
"type": "registry:block",
"title": "Feedback Plugin",
"description": "Add a feedback popup to your site with admin dashboard.",
"dependencies": ["zod", "@hookform/resolvers", "react-hook-form"],
"files": [
{
"path": "packages/plugins/feedback/src/components/feedback.tsx",
"type": "registry:file",
"target": "packages/plugins/feedback/src/components/feedback.tsx",
"content": "// full source code embedded as a string..."
}
]
}

The key insight: shadcn build takes your plugin source code and embeds it as strings inside JSON. Our CLI then fetches this JSON, iterates over the files array, and writes each entry to its target path. It also resolves dependencies and installs them via the user's package manager.

We get the benefits of shadcn's build tooling and registry schema without being constrained by its CLI's limitations.

Our registry lives at makerkit.dev/r/{variant}/ and serves plugins as static JSON. The variant segment (next-supabase, next-drizzle, next-prisma) ensures users get the right files for their stack. The Next.js + Supabase variant ships with the full catalog of thirteen plugins. Next.js + Drizzle and Next.js + Prisma currently support six plugins (analytics, monitoring, and CMS), with the remaining plugins being ported.

Layer 2: Codemods for config wiring

File distribution only gets you halfway. After the CLI copies a feedback plugin into packages/plugins/feedback/, the plugin still isn't wired into your app. You'd need to:

  • Import it in your plugin config
  • Register the provider
  • Add routes or navigation entries
  • Update any middleware

Doing this with string manipulation (regex, find-and-replace) is brittle. A single formatting change breaks the match. Multi-line insertions are error-prone.

We use Codemod.com with JSSG (JavaScript Syntax Grep) for AST-based transforms instead. AST transforms understand the structure of your code, not the formatting. They can find a specific object property, a specific import statement, or a specific function call, regardless of whitespace, trailing commas, or comment placement.

Here's a simplified example of a JSSG transform that registers a Google Analytics provider in the plugins config (the production version handles additional edge cases):

import type { SgRoot, Edit } from "@codemod.com/jssg-types/main";
async function transform(root: SgRoot): Promise<string | null> {
const rootNode = root.root();
const edits: Edit[] = [];
// Idempotency: if already imported, skip
const existingImport = rootNode.find({
rule: {
kind: "import_statement",
has: {
kind: "string",
has: {
kind: "string_fragment",
regex: "^@kit/google-analytics$",
},
},
},
});
if (existingImport) {
return null; // Already installed, no changes needed
}
// Add import before the first existing import
const firstImport = rootNode.find({
rule: { kind: "import_statement" },
});
if (!firstImport) return null;
edits.push({
startPos: firstImport.range().start.index,
endPos: firstImport.range().start.index,
insertedText:
"import { createGoogleAnalyticsService } from '@kit/google-analytics';\n\n",
});
// Find the providers object and add entry
const providersObject = rootNode.find({
rule: {
kind: "object",
inside: {
kind: "pair",
has: {
field: "key",
kind: "property_identifier",
regex: "^providers$",
},
},
},
});
if (!providersObject) return null;
edits.push({
startPos: providersObject.range().start.index + 1,
endPos: providersObject.range().end.index - 1,
insertedText:
"\n 'google-analytics': createGoogleAnalyticsService,\n ",
});
return rootNode.commitEdits(edits);
}

Notice the idempotency check at the top. If the import already exists, the transform returns null (no changes). This means you can safely re-run a codemod without duplicating entries. This is what makes plugins update safe to run repeatedly.

Each codemod package contains a workflow.yaml that chains the file download step with AST transforms:

version: "1"
nodes:
- id: install-files
name: Install plugin files
type: automatic
steps:
- name: "Download and write plugin files"
command: "npx @makerkit/cli@latest plugins download feedback"
- id: register-plugin
name: Register plugin in config
type: automatic
depends_on: [install-files]
steps:
- name: "Add plugin to config"
js-ast-grep:
js_file: scripts/codemod.ts
language: "tsx"
include:
- "packages/plugins/src/index.ts"

The codemod handles both file distribution and AST transforms in a single invocation. The CLI makes one call instead of two.

CLI orchestration

The MakerKit CLI ties it all together. Here's a condensed view of what plugins add does under the hood:

// 1. Validate git is clean
const gitClean = await isGitClean();
if (!gitClean) {
console.error('Please commit or stash changes first.');
process.exit(1);
}
// 2. Detect variant from package.json dependencies
const { variant } = await validateProject();
// 3. Auto-init registry if needed
if (!(await isMakerkitRegistryConfigured())) {
await addMakerkitRegistry(variant);
}
// 4. Validate plugin + check if already installed
const registry = await PluginRegistry.load();
const plugin = registry.validatePlugin(pluginId, variant);
if (await isInstalled(plugin, variant)) {
console.log(`Plugin "${plugin.name}" is already installed.`);
return;
}
// 5. Run codemod (handles file download + AST transforms)
const result = await runCodemod(variant, pluginId);
// 6. Append env vars to .env.local and .env.example
const envVars = getEnvVars(plugin, variant);
await appendEnvVars(envVars, plugin.name);

A few design decisions worth calling out:

Variant auto-detection. The CLI reads package.json dependencies across apps/web and packages/database to determine your stack. It checks for @supabase/supabase-js, drizzle-orm, @prisma/client, and @react-router/node. You never have to tell it which variant you're using.

Filesystem-based installation detection. Instead of maintaining a manifest file (.makerkit-plugins.json), we detect installed plugins by checking if the plugin directory exists on disk. One less file to commit, zero state to keep in sync.

Remote plugin catalog. The plugin catalog can be fetched from a remote URL (MAKERKIT_PLUGINS_REGISTRY_URL env var), with a bundled default as offline fallback. This lets us add new plugins or variant support without shipping a CLI update. The catalog is cached at ~/.makerkit/plugins.json with a 1-hour TTL.

Clean git enforcement. The CLI requires a clean working tree before making any changes. If the codemod fails halfway through, recovery is just git checkout . && git clean -fd. No custom rollback logic needed.

Why not just npm packages?

You might wonder: why not publish plugins as npm packages and let users pnpm add @makerkit/feedback?

Because you'd lose the ability to customize. Plugin code lives in your monorepo, not in node_modules. You can read it, modify it, debug it. When you need to change how the feedback widget looks or how the testimonial form validates, you edit the source directly.

npm packages are great for stable, generic libraries. Plugin code is intentionally project-specific. You're meant to own it and evolve it alongside your app.

Why not just shadcn alone?

Shadcn CLI doesn't support nested file paths. It works great for flat component directories like components/ui/button.tsx, but a MakerKit plugin spans deeply nested paths across src/components/, src/lib/server/, migrations/, and more. We needed our own orchestrator to handle that.

Build and publish pipeline

For the curious, here's how plugins get from source code to the registry:

  1. Plugin source code lives on a plugins branch in the kit repo (e.g., kit-supabase:plugins)
  2. Running npx shadcn build compiles all plugin source into JSON files with embedded content
  3. Built JSON files are copied to the registry repo and pushed
  4. The registry auto-deploys on push (it's a Next.js app on Vercel)

The whole process is manual and intentional. No CI pipelines to maintain or debug. When we're ready to ship a plugin update, we build, copy, push. Simple. And when you run plugins update on your end, you get the new version in seconds.

What's next

We're just getting started. Here's what's coming:

  • React Router variant: The remaining variant will get plugin support soon
  • More plugins: New plugins based on customer requests — tell us what you need

Frequently Asked Questions

How do I install a MakerKit plugin?
Run npx @makerkit/cli plugins init to set up the registry, then npx @makerkit/cli plugins add <plugin-name>. The CLI handles file distribution, config wiring, and environment variables automatically.
Do I need a MakerKit license to use plugins?
Yes. The plugin registry is protected by GitHub organization membership. Your GitHub username must be part of the MakerKit organization, which happens automatically when you purchase a license.
Can I customize plugin code after installing?
Absolutely. Plugin code lives in your monorepo under packages/plugins/, not in node_modules. You own it and can modify anything.
What happens if the plugin installation fails?
The CLI requires a clean git state before installing. If anything goes wrong, run git checkout . && git clean -fd to revert all changes instantly.
Which MakerKit variants are supported?
Next.js + Supabase has the full catalog of thirteen plugins. Next.js + Drizzle and Next.js + Prisma currently support six plugins (analytics, monitoring, and CMS), with the rest being ported. React Router + Supabase plugin support is coming soon.
How do codemods work? Will they break my code?
Codemods use AST (Abstract Syntax Tree) transforms via JSSG, not regex or string replacement. They understand your code structure and are idempotent, so running them twice produces the same result. They also check for existing imports before adding new ones. While they're tested, you should review the changes before committing as your code may have changes from the upstream kit.
How do I update a plugin to a newer version?
Run npx @makerkit/cli@latest plugins update to update all plugins, or plugins update feedback to target a specific one. The CLI lists modified files and asks for confirmation before overwriting. Use plugins diff to preview changes first.