CLI
The flowpanel command-line interface.
FlowPanel ships a small CLI in the @flowpanel/cli package. Bootstrap a
project with pnpm dlx @flowpanel/cli init — no install needed up front.
init adds @flowpanel/cli as a devDependency, so every later command
runs as pnpm flowpanel <command>. The shipped subcommands are init,
migrate, doctor, eject, dev, and new.
The CLI detects your package manager from the lockfile and uses it
throughout — init installs with it, dev launches Next.js through its
runner, and every "run this next" hint is printed in its syntax. The samples
below use pnpm; substitute your own.
| Command | What it does |
|---|---|
flowpanel init [options] | Initialize FlowPanel in this project |
flowpanel migrate [options] | Apply SQL migrations from flowpanel/migrations/ |
flowpanel doctor [options] | Check FlowPanel health and wiring |
flowpanel eject [options] <target> [name] | Eject a FlowPanel piece into your app/admin folder. Targets: resource <name>, dashboard <path>, layout. |
flowpanel dev [options] | Run Next.js dev server (and bull-board if REDIS_URL is set) |
flowpanel new [options] <resource> | Add a resource(...) entry to flowpanel.config.ts |
Every command supports --help; the root command supports --version.
Success exits 0, validation or execution failures exit non-zero, and fatal
errors show a concise message. Set FLOWPANEL_DEBUG=1 when you also need the
underlying stack trace.
Mutating commands build a complete filesystem plan before the first write.
--dry-run prints that plan; --json emits a machine-readable result for CI.
Writes use atomic replacement and a rollback journal, so a later failure
restores earlier files instead of leaving a half-written scaffold. Existing
files with different content are reported as conflicts, not overwritten.
flowpanel init
flowpanel init [options]| Option | Description | Default |
|---|---|---|
--yes | Accept detected defaults without prompting (CI mode) | — |
--dry-run | Print the filesystem plan without writing or installing | — |
--json | Emit machine-readable JSON (implies --yes) | — |
--path <url> | Mount the admin at a static URL, for example /ops/admin | — |
--db <module> | Local module exporting your db or prisma client | — |
--schema <module> | Local module exporting your Drizzle schema | — |
--auth <module> | Local module exporting getSession(request) | — |
--dev-auth | Explicitly use an open development-only admin identity | — |
Detect your stack (Next.js, Drizzle/Prisma, auth helper), pick the
matching config template, install @flowpanel/kit + @flowpanel/cli,
and scaffold the wiring files. Both Drizzle and Prisma are supported in a
single command; the CLI picks based on your installed dependencies
(Drizzle wins if both are present).
The stylesheet lands at styles/admin.css, or at src/styles/admin.css when
the project keeps its source under src/ — the CLI matches your layout rather
than always writing to the repo root.
◇ Detected stack ─────────────────────────────────────╮
│ Next.js 16.3.1 · TypeScript · Drizzle · Tailwind 4 │
├──────────────────────────────────────────────────────╯
◇ 7 written
◇ Wrote ─────────────────────────────────────────────────╮
│ flowpanel.config.ts │
│ app/admin/[[...slug]]/page.tsx │
│ app/api/flowpanel/[...route]/route.ts │
│ app/api/flowpanel/stream/route.ts │
│ styles/admin.css │
│ flowpanel/migrations/0001_init.sql │
│ app/admin/layout.tsx (imports the admin stylesheet) │
├─────────────────────────────────────────────────────────╯
◇ Installed @flowpanel/kit, @flowpanel/cliWhat varies:
src/— everyapp/path becomessrc/app/, andstyles/admin.cssbecomessrc/styles/admin.css, when the project uses asrc/layout.- Admin mount —
/adminby default,/flowpanelwhen/adminis occupied, or an explicit--path /ops/admin. Route groups are included in collision checks; a busy fallback stops installation before any writes. - Admin layout — imports the stylesheet under the chosen route. The existing
root layout is never patched. Without a shared root layout, the admin segment
receives its own root layout. That layout reads the
x-noncerequest header and forwards it toThemeScript, so the pre-hydration theme script survives a strictscript-srcpolicy — but only when your own middleware setsx-nonceon the request and emits the matchingscript-src 'nonce-…'header. FlowPanel does not write that middleware, and with nox-noncepresent the header read returnsnulland the script is rendered without a nonce, exactly as before. Readingheaders()makes the admin segment dynamically rendered; it already was, becauseauth.sessionreads the request. Existing host theme/CSP integration is left to the application;ThemeScriptaccepts its nonce. - Authentication —
initreadspackage.jsonforbetter-auth,next-auth,@clerk/nextjsorluciaand names what it recognised in the detected-stack note and the outro. When the auth module it found exports that provider's own instance rather than agetSession, it writesserver/lib/flowpanel-session.tsbuilt on the matching preset (withBetterAuth,withClerk,withNextAuth,withLucia) and points the config at it. A module that already exportsgetSessionis left alone, and an unrecognised project still gets the closedgetSessionadapter. Connect your role mapping either way; async role lookups are supported. Use--dev-authonly to explicitly opt into an open development-only identity. - Stylesheet —
admin.cssimports@flowpanel/kit/styles/admin.css, the precompiled and isolated FlowPanel stylesheet. It does not require Tailwind, a Tailwind config, source/content directives, or a PostCSS plugin. - The install step uses the package manager it detected — npm, pnpm, yarn
or bun — and is skipped for packages already in your
package.json. - The next-steps outro is written in that manager's own vocabulary
(
pnpm exec flowpanel migratevsnpx flowpanel migrate, and so on).
When init stops
- No Next.js in
package.json— aborts, exit1, with the install command for your package manager. - No Drizzle and no Prisma — aborts, exit
1, same treatment. - No TTY and no
--yes,--dry-runor--json—initasks questions, so a non-interactive run without--yesexits1immediately: "init asks questions and this run has no interactive terminal. Re-run with --yes to accept the detected defaults." Pass--yesin CI. - A failed dependency install does not roll back the files. It prints the
package manager's own last lines, the commands to run by hand, and sets
exit code
1so the run is not mistaken for a success. - A dependency that installs but cannot be used is treated the same way. A
@flowpanel/kitfrom another minor, or a project-local@flowpanel/clifrom another minor than the one you ran, is listed independencies.failedwith its own recovery command and exits1. A different patch or a matching preview of the same minor is accepted.
Re-running init is idempotent: matching generated files become skip.
If any target exists with different content, the plan reports a conflict and
nothing is written. Merge that file manually, then rerun. --yes accepts
detected paths; it does not grant permission to overwrite source.
flowpanel migrate
flowpanel migrate [options]| Option | Description | Default |
|---|---|---|
--dry-run | Print migrations that would be applied without running them | — |
--json | Emit machine-readable JSON | — |
Apply SQL migrations from flowpanel/migrations/. The CLI dynamically
imports your flowpanel.config.ts (via jiti) so it can reach the
adapter's db instance and run the migration there.
$ pnpm flowpanel migrateUse --dry-run to inspect migration files without importing your config or
opening a database connection. Because it deliberately cannot know the applied
state, it lists every SQL file and reports appliedStateKnown: false in JSON.
Run the command without --dry-run to compare those files with the database.
Add --json for a stable pending/applied payload. The CLI tracks applied IDs in
_flowpanel_migrations.
Each pending file is handed to the selected migration executor. A modern
adapter provides one applyMigration(id, sql) operation; the compatibility
executor composes the legacy runMigrationSql and markMigrationApplied
hooks. On PostgreSQL and SQLite the modern operation keeps SQL and bookkeeping
in one transaction. MySQL DDL implicitly commits, so review and back up
production migrations.
The applied-ID list is a preflight snapshot, not a process lock. Both shipped adapters serialize at the database, recheck the ID under that lock and treat a concurrent duplicate as a no-op. PostgreSQL and SQLite keep the file and its marker in one transaction. MySQL implicitly commits DDL, so it is serialized instead: Drizzle pins a connection behind an advisory lock, Prisma claims a durable row that also records where a failed run stopped.
Custom adapters should implement applyMigration and listAppliedMigrations.
During the 0.2 upgrade, the CLI still accepts the legacy runMigrationSql plus
markMigrationApplied pair and warns that it is non-atomic and does not
serialize concurrent migrators; JSON output then exposes
migrationMode: "legacy-hooks" and a non-empty warnings array, so CI can
reject it explicitly. Both shipped adapters implement the locked operation and
never take that path.
Both adapters reject dialect-specific procedural scripts without PostgreSQL
dollar-quoted bodies before executing them; Prisma also rejects MySQL
executable comments and client directives such as DELIMITER. Use the ORM's
native migration workflow for those files.
Every command reads .env.local and then .env from the working directory
before it runs, the same order Next.js uses, so a DATABASE_URL that already
works for next dev works here too. Variables already set in the shell win, so
DATABASE_URL=… pnpm flowpanel migrate still overrides the file. When the
connection fails and no DATABASE_URL is set anywhere, the CLI says so instead
of printing a driver stack trace; FLOWPANEL_DEBUG=1 brings the stack back.
jiti is loaded on demand; when it is missing the command prints the install
line for your package manager (pnpm add -D jiti, npm install --save-dev jiti, …) and exits 1.
flowpanel doctor
flowpanel doctor [options]| Option | Description | Default |
|---|---|---|
--fix | Auto-fix missing route files from templates | — |
--dry-run | Show fixes without writing (use with --fix) | — |
--json | Emit machine-readable JSON | — |
--path <url> | Resolved admin URL when the config uses a dynamic path | — |
Run the wiring checks, in this order:
- Next.js ≥ 16.3 and < 17, and TypeScript installed
- an ORM the adapters support — Drizzle or Prisma
flowpanel.config.tsexists- the four generated files exist: the API route, the SSE route, the seed migration, and the catch-all admin page
- only one reachable
@flowpanel/coreinstallation across the installed dependency graph, including package-local peers and third-party plugins. Stale pnpm store entries are ignored; uninstalled projects skip this check - no destructive row/bulk action declared without a
confirm tsc --noEmitpasses
Mount checks follow URL segments through route groups, parallel slots, and
intercepting routes. Existing pages inside the selected admin branch remain
protected. Sibling dynamic routes such as [locale] and parent catch-all fallbacks
can coexist with a static admin branch; Next gives the static branch precedence.
The selected admin URL is therefore reserved for FlowPanel even if a fallback
previously matched it.
$ pnpm flowpanel doctor
$ pnpm flowpanel doctor --fix
$ pnpm flowpanel doctor --fix --dry-run--fix creates whichever of the four generated files are missing from the
same templates init uses. The fix is one transaction; --dry-run shows it
without writing. The other failures have no auto-fix and print a hint instead.
Exit code is non-zero when any check fails — wire it into CI to catch drift on every PR.
flowpanel eject
flowpanel eject [options] <target> [name]| Argument | Description | Status |
|---|---|---|
<target> | — | required |
[name] | — | optional |
| Option | Description | Default |
|---|---|---|
--force | Overwrite files if they already exist | — |
--dry-run | Print the filesystem plan without writing | — |
--json | Emit machine-readable JSON | — |
Eject one of three targets — resource, dashboard, or layout —
into your repo. See Customization / Eject.
$ pnpm flowpanel eject resource users
$ pnpm flowpanel eject dashboard "/monitoring"
$ pnpm flowpanel eject layoutIt lists every file it wrote, then comments the matching config entry out
(uncomment to revert). Template files and the config edit are committed as one
rollback-safe transaction. Pass --force to explicitly replace an existing
eject target; without it, a conflict aborts before any file changes.
flowpanel dev
flowpanel dev [options]| Option | Description | Default |
|---|---|---|
--port <port> | Next.js dev port | 3000 |
--no-board | Skip the bull-board server even if REDIS_URL is set | — |
Wrapper around next dev that also spawns bull-board when a
scripts/board-server.mts (or .ts) file is present and REDIS_URL is set.
$ pnpm flowpanel dev --port 3000Both children are launched through the detected package manager's runner —
pnpm exec next dev, npx next dev, yarn next dev, bunx next dev — so
the local binaries resolve whichever manager installed them. Output is
prefixed [next] / [board], and either child exiting shuts the other down.
Pass --no-board to skip the queue dashboard even when Redis is up. With
REDIS_URL set but no scripts/board-server.mts or .ts on disk, the board is skipped
with a one-line note rather than an error.
flowpanel new
flowpanel new [options] <resource>| Argument | Description | Status |
|---|---|---|
<resource> | — | required |
| Option | Description | Default |
|---|---|---|
--table <expr> | First argument expression (default: schema.<resource>) | — |
--kind <kind> | Adapter kind: drizzle (default) or prisma | drizzle |
--dry-run | Print the filesystem plan without writing | — |
--json | Emit machine-readable JSON | — |
Append a resource(...) entry to your existing
flowpanel.config.ts without re-running init.
$ pnpm flowpanel new posts└ Added resource "posts" to flowpanel.config.tsIt writes resource(schema.posts, { columns: ["id"] }) into the resources
array — creating the array when defineAdmin has none — and adds resource
to the existing @flowpanel/kit import when the config does not already
bind that name, so the file compiles as written. Existing comments in the
array are preserved (the entry is spliced as text, not reprinted).
Before writing, a Drizzle-kind run resolves the config's import * as schema
line and checks that the module really exports posts. A miss aborts with
exit 1 and lists what the schema does export; an unverifiable case (no
import * as schema, an unresolvable specifier) warns and writes anyway.
A resource that already exists aborts with exit 1.
Flags: --table <expr> overrides the first argument (default
schema.<resource>, and skips the schema check); --kind drizzle|prisma
toggles the template (default drizzle; prisma writes
resource<unknown>("posts", …)).
--dry-run shows the config modification without applying it. --json
returns the same plan/result envelope used by init, doctor and eject.