Documentation
Everything the CLI does, explained.
Using modules in your own project is public and needs no account. Publishing your own is a separate flow that does. Plus what goes in a manifest, how naming stays conflict-free, and how versions and updates work.
01 · Quickstart
Using modules
Installing modules into your own project is completely public. Anyone can browse the registry and add modules to their project without an account, a token, or signing in anywhere.
0. Get the CLI
The CLI lives inside the NextAPI framework repo itself. Clone it and install dependencies once:
Then link it so the nextapi command is available everywhere:
Prefer not to install it globally? Every command below also works prefixed with npm run nextapi --, e.g. npm run nextapi -- add profile-page.
1. Add a module
Browse the /registry for something you want, then add it to your project. No login required, this just records what you want in your own project's lockfile:
2. Install it
Fetches and installs everything you've added, on any machine, even one that's never seen this module before:
If the module you're adding needs other modules, they're fetched and installed automatically. You don't need to add each one yourself.
That's it for using modules. Publishing your own is a separate flow, covered next, and does need an account.
01.5 · How it works
Linking modules to real collections
A module can declare a Mongo collection (e.g. "users") that overlaps with a collection the host app already owns. Without any special handling, that module creates its own, separate collection with that name - one with no connection to who can actually log into the app.
nextapi sync checks for this at install time. If a module's own model fields are compatible with a collection the host already has, it offers to link the module to the real data instead of creating a duplicate:
'@alice/user-management' declares a "users" collection compatible with the
host's existing UserCollection. Link it instead of creating a separate collection? (y/N)Confirm and the module's generated code is rewritten to call the host's real collection helper - no second collection, no drift between what the module knows about and what's actually real. Pass --yes to confirm automatically for non-interactive runs.
When the fields don't match
Sync doesn't fail. The module installs against its own collection exactly as it always would, and an end-of-run report names precisely which fields the module expects that the host collection doesn't have (or has with a different type) - something concrete for the module's author to fix and republish.
The decision is remembered. Installing a new version of an already-linked module re-links it automatically - a user isn't asked again every time they update. If a new version's schema stops matching, it falls back to its own collection instead of staying linked to something it no longer fits, and sync warns explicitly: anything the module already wrote through the link stays in the host's real collection, but new writes go to its own collection until it's re-linked.
Matching fields isn't the whole story. A module can also call methods on its collection class (UserCollection.count(...), .list_page(...)) that the host's real UserCollection doesn't have. When that happens, sync copies the module's own implementation of exactly those methods into the host's real models_mongo.py - purely additive, never touching or overwriting a method the host already has, since something else (like /auth/register) may already depend on it behaving exactly as it does today:
Linked '@alice/user-management' to the host's existing UserCollection.
Adopted into UserCollection: count, list_page
(these are now part of the host's UserCollection - inspect backend/models_mongo.py)Field compatibility also can't catch data that predates the field: a users collection may hold documents written before a field existed on the host's model, especially if that model was never enforced on every write path. Sync samples the host's actual MongoDB collection at link time and, if a field the host's model declares required is missing from real, existing documents, asks explicit consent before demoting that field to optional in the host's schema:
'users' collection: 'created_at' is declared required, but missing from
214/1,048 existing documents. Demote 'created_at' to optional in the
host's UserMongo model to match reality? (y/N)Declining a demotion
Aborts the entire link for that module - nothing is written to models_mongo.py, and the module falls back to its own separate collection, same as an incompatible-fields case. This check needs the host's Mongo reachable from the machine running sync; if it isn't, sync skips it silently and links exactly as it would have before this check existed.
Building a module that manages users?
Match the host's real field names and types up front - a model with username, email, role, and hashed_password links cleanly instead of falling through to its own disconnected collection. This is host-specific - check the target project's own docs for its exact field names before publishing. And once linked, don't assume every existing document has every field: read anything sourced from a host-owned collection with doc.get("field"), not doc["field"], and type it Optional[...] in the response schema - the host's real data can predate the module by years.
Installing a public module that doesn't link? Fork it - by hand, or by handing the field-mismatch report to the AI Builder and asking it to update the module's model to match - then republish. A module that links correctly once keeps linking for everyone who installs it after that.
01.6 · How it works
Overwriting an existing route
Some modules aren't a new page alongside what a project already has - they're a replacement for it. A custom login screen, a different landing page, a redesigned register flow. A route entry can opt into this with overwriteRoute: true:
"frontend": {
"routes": [
{ "path": "/login", "sourceDir": "frontend/app/login",
"overwriteRoute": true, "overwriteRouteAck": true }
]
}overwriteRouteAck: true is required alongside it - the registry rejects a publish that sets overwriteRoute without it. It exists so replacing existing content is something a module author had to deliberately confirm in the manifest itself, not something a CLI prompt alone can be scripted past.
The first time anything overwrites a given path on the installing project, its sync warns explicitly and asks to confirm:
'@alice/custom-login' wants to REPLACE your existing content at '/login'. This is
normally irreversible - your current files there will be backed up, but only
restorable via `nextapi rollback /login`, a separate explicit command. Continue? (y/N)Confirm, and the project's current content at that path is captured first - not just deleted. The capture is a real snapshot: the original files, plus enough bookkeeping that database and backend wiring still work correctly if the route is ever rolled back. Only after that capture succeeds does the new module's content actually replace what was there.
Undoing it
nextapi rollback /login removes whatever currently occupies /login and restores exactly what was captured there before it was first overwritten. This is the only supported way back - there is no automatic or silent restore, and nextapi remove on the overwriting module does not bring the original content back by itself (it just leaves the route empty until something else is placed there).
Overwriting a path already occupied by another installed module (rather than the project's original, native content) works the same way: that module is removed first, then the new one takes its place. Rolling back later always restores whatever was captured at the very first overwrite of that path, regardless of how many modules have occupied it since.
This requires MongoDB (DATABASE_TYPE=mongo in backend/.env) on the installing project from the first overwrite onward, same as any other installed module.
02 · Quickstart
Publishing modules
Publishing is the one part of this workflow that needs an account. You'll need an access token to prove the CLI is acting on your behalf, it keeps your account secure without asking you to type your password every time.
1. Create a token
Go to /settings/tokens while logged in and create a token, then paste it into the CLI:
2. Publish a module
Run this from inside a folder containing a module.json. It bundles up the folder and uploads it as a new version.
The first time you publish a given id, it's registered to your account automatically. From then on, only you can publish new versions of it.
02 · Reference
CLI reference
nextapi loginPaste the client_id:client_secret token you created at /settings/tokens. It's saved to .nextapi/auth.json so you only do this once per machine.
nextapi publish [dir]Publishes the module in dir (defaults to the current folder) as a new version. The first time you publish a given id, it's registered automatically. Once a version is published it can't be changed or replaced, publish a new version number instead.
nextapi add <id | id@range | path>Adds a module to your project. Three ways to call it:
nextapi add profile-page: get the latest version.nextapi add profile-page@^1.0.0: get a specific version or range.nextapi add ./my-local-module: add a module from a folder on your own machine.
nextapi syncInstalls everything you've added. If a module needs other modules to work, those are fetched and installed for you too, so you won't need to track down dependencies by hand.
nextapi remove <id> [--force] [--purge-env]Removes a module from your project. If you've edited its files yourself, it'll ask you to confirm with --force so you don't lose changes by accident. Add --purge-env to also clean up any environment variables it added.
nextapi listShows everything installed, and flags anything that's been changed since it was set up.
nextapi rollback <path>Undoes an overwriteRoute overwrite of path, restoring what was there before it was first overwritten. See Overwriting an existing route above.
03 · Schema
The module.json manifest
Every module is described by a single manifest file at its root. Here's a real one:
{
"id": "ai-notes",
"version": "1.0.0",
"nextapiVersion": ">=1.0.0 <2.0.0",
"displayName": "AI Notes",
"description": "Adds a protected /notes page...",
"category": "ai",
"author": { "name": "NextAPI", "url": "" },
"license": "MIT",
"requires": { "modules": ["profile-page@^1.0.0"] },
"env": {
"backend": [
{ "key": "ANTHROPIC_API_KEY", "required": true, "secret": true }
],
"frontend": []
},
"dependencies": {
"frontend": { "npm": { "react-markdown": "^9.0.0" } },
"backend": { "pypi": { "anthropic": "^0.40.0" } }
},
"frontend": {
"routes": [{ "path": "/notes", "protected": true, "label": "Notes" }]
},
"backend": {
"router": { "module": "backend/router.py", "attr": "router", "prefix": "/notes" },
"collection": "module_ai_notes"
},
"functions": {
"uses": ["auth.get_current_user", "profile.get_display_name"]
},
"postInstall": { "message": "Set ANTHROPIC_API_KEY, then run `nextapi sync`." },
"notes": "A full notes page with AI summarization, not just a text field."
}| Field | Type | Description |
|---|---|---|
| id* | string | The module's unique name. Plain names like "profile-page" are reserved for official modules, everyone else uses a scoped name like "@alice/profile-page". |
| version* | semver | A version number in MAJOR.MINOR.PATCH form, optionally with a -prerelease tag. Once published, a version can't be changed. |
| nextapiVersion* | range | Which app versions this module works with, e.g. ">=1.0.0 <2.0.0". |
| displayName | string | The name shown in the registry. |
| description | string | Shown in search results and on the module's page. |
| category | string | A grouping used for browsing, e.g. "core", "ai", "auth". |
| author.name / author.url | string | Who made it, shown on the module's page. |
| license | string | The license it's released under, e.g. "MIT". |
| requires.modules | string[] | Other modules this one needs, written as "id@range", e.g. "profile-page@^1.0.0". |
| shareable | boolean | Defaults to true. Set false only if this module can safely run as more than one installed version at once - see Versions below. |
| env.backend / env.frontend | object[] | Environment variables the module needs ({ key, required, secret, description }), added for you when it's installed. |
| dependencies.frontend.npm | object | Any npm packages the module needs, added automatically on install. |
| dependencies.backend.pypi | object | Any Python packages the module needs, added automatically on install. |
| frontend.routes | object[] | The pages this module adds: path, source folder, whether it needs sign-in, and who can see it. |
| frontend.routes[].overwriteRoute | boolean | Replaces whatever currently lives at that path on the installing project (its own content, or another module's) instead of installing alongside it. Normally irreversible except via nextapi rollback - see Overwriting an existing route above. |
| frontend.routes[].overwriteRouteAck | boolean | Required alongside overwriteRoute: true - an explicit, in-manifest confirmation that replacing existing content is intentional. The registry rejects a publish missing it. |
| backend.router | object | Where the module's server-side code lives and which URL prefix it responds to. |
| backend.models | object | Where the module's database model lives. |
| backend.collection | string | The database collection this module stores its data in. If this names a collection the installing project's host already owns (e.g. "users") and your model's fields are compatible, sync offers to link to the real data instead of creating a duplicate - see Collection linking above. |
| schemaVersion | integer | Only meaningful alongside backend.collection. Bump it when the shape of an already-stored document changes, separately from version - see Versions below. |
| functions.uses / functions.provides | string[] | Functions this module calls from other modules, or makes available for others to call. |
| postInstall.message | string | A message shown after the module is installed, handy for any manual setup steps. |
| notes | string | Optional. A short note shown to visitors on the module's Details tab before they install it, e.g. what kind of experience it is or a dependency worth knowing about. Plain language, no marketing copy. |
| implementationNotes | string | Required. Longer, maintainer-facing documentation: what's mocked and needs wiring to something real, what this module assumed about the host's schema/environment, and any setup beyond install. Minimum 200 characters, rejected by the registry if missing or too short. |
* Required.
04 · Naming
Names never clash
Nothing stops two different people from wanting to call their module profile-page. To keep every name unique, plain names are reserved for official modules, and everyone else publishes under their own username.
profile-page
Reserved for official modules only.
@alice/profile-page
Everyone else publishes under their own username.
A unique name on its own isn't quite enough, since two people's modules could still both want the page /profile. So when a module is installed, anything that could clash is namespaced automatically:
- Pages:
/profileinstalls at its own unique path. Nobody browses an installed app by typing raw URLs, and the sidebar/nav label always comes from your module's own display name, so there's no reason for an installed route to expose whose module it is - Server-side code: kept under its own separate namespace per module
- Database storage: kept separate per module too, even if two authors used the same names
You can write your module as if it were the only one that will ever exist. The rest is handled for you when it's installed - even down to two of your own modules that happen to declare the same route.
05 · Versions
Versions and updates
Version numbers
Every version needs a proper number in MAJOR.MINOR.PATCH form, optionally with a -prerelease tag. Anything else is rejected when you publish.
Once published, it's final
You can't change or replace 1.2.0 after it's out, publish a new version instead. You can still pull an old one from circulation (see yanking, below).
"Latest" always means latest
Based on version number, not publish order. If 1.5.0 comes out after 2.0.0, people installing "latest" still get 2.0.0.
Yanking
Pulls a version out of search and new installs without deleting it. Anyone already using it isn't affected.
When you need a specific version of a module, or a module depends on one, you can write it as:
| ^1.2.0 | Anything compatible with 1.2.0: same major version, same or newer. |
| >=1.0.0 <2.0.0 | Anything within a specific range. |
| (nothing) | Just get the latest version. |
Nothing to track down by hand
If a module you're adding or syncing needs a version that isn't on your machine, it's fetched automatically. If the requested version isn't available, the error message lists what is.
When two modules need different versions
By default, every module keeps exactly one installed version at a time. If two of your installed modules depend on the same module at incompatible majors, nextapi sync resolves compatible-but-different ranges to one shared version automatically, and only surfaces a real conflict - naming both requirements - when a module you rely on genuinely can't satisfy both at once.
A module that would rather support multiple installed majors side by side than force that choice can opt in with "shareable": false. Each dependent then gets wired to whichever version it actually declared, automatically, with no manual resolution needed. This only makes sense for a module with no persisted state of its own - two live copies of something meant to be a single shared source of truth would fork the data, not share it, so a module that owns a backend.collection should leave shareable at its default.
For a module that does own persisted data, the shape of what's already stored is versioned separately from the module itself, via schemaVersion. Bumping it requires publishing a normalizer (functions.provides) that upgrades an older document to the current shape on read, so your code's own version can move forward freely without ever leaving a previously-stored document unreadable.
06 · Builder
Building in the browser
You don't need a local project to make a module. The /builder page lets anyone with an account write one in the browser and publish it.
Start from a working module
Choose Start new module, then give it a name, a display name and a category (pick one, or type your own). You get a complete starter - a backend, a page and a manifest - that already runs, so you edit rather than start from a blank file.
Drafts wait for you
Unfinished modules are saved as drafts. Leave and come back from the Builder page whenever you like; a draft stays until you publish it or delete it. Published modules go out as @your-username/name.
Before you can publish
The starter's implementationNotes describe the unmodified starter and end with a sentence beginning TEMPLATE:. Rewrite the notes to describe your module and remove that sentence - an untouched template can't be published.
Forking and credit
Fork & Edit on any public module copies it into a draft. The original author is recorded by the registry and shown on your module's page as "Forked from ... original by @author". That credit can't be removed by editing the draft. Forking your own module just starts a new version of it.
The code editor
Files open in a full code editor (the same engine as VS Code), served from this site with no third-party requests. It understands the languages a module is written in:
- TypeScript / JavaScript (
.ts .tsx .js .jsx): syntax highlighting, completion and live error checking as you type. Imports from the host app (such as@/components/ui/button) aren't flagged, since the editor can't see them - Python (
.py): highlighting, plus syntax errors marked on the exact line each time you save. A module whose Python doesn't parse can't be published - JSON (
module.json): highlighting and live syntax checking - Any other file type opens as plain text and is never blocked
Press Ctrl/Cmd + S to save. Saving re-runs the module's checks and shows the result above the editor; Preview renders the page with sample data.
Admins also have the AI Builder at /ai-builder, where Claude writes the module from a description. It's a separate page with its own drafts, and its modules can be opened in the same editor.
Keeping this free
The registry, the CLI, and the AI Builder all run on real infrastructure and real API costs.
Hosting the registry is the easy part. The AI Module Builder calls Claude on every message, every generated file, every screenshot you attach, that's a real, ongoing bill, not a one-time cost. None of it is paid for by ads, by selling your data, or by a "free tier that quietly gets worse." It's paid for directly, so it can stay that way.
Registry & hosting
Every module, every version, every tarball, served instantly, forever.
AI Builder usage
Claude API calls for every module the Builder scaffolds, on the house.
Staying open source
No paywalled core features, no rug pull. Sponsorship is what makes that sustainable.