Browse documentation

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.

CommandWhat 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]
OptionDescriptionDefault
--yesAccept detected defaults without prompting (CI mode)—
--dry-runPrint the filesystem plan without writing or installing—
--jsonEmit 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-authExplicitly 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/cli

What varies:

  • src/ — every app/ path becomes src/app/, and styles/admin.css becomes src/styles/admin.css, when the project uses a src/ layout.
  • Admin mount — /admin by default, /flowpanel when /admin is 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-nonce request header and forwards it to ThemeScript, so the pre-hydration theme script survives a strict script-src policy — but only when your own middleware sets x-nonce on the request and emits the matching script-src 'nonce-…' header. FlowPanel does not write that middleware, and with no x-nonce present the header read returns null and the script is rendered without a nonce, exactly as before. Reading headers() makes the admin segment dynamically rendered; it already was, because auth.session reads the request. Existing host theme/CSP integration is left to the application; ThemeScript accepts its nonce.
  • Authentication — init reads package.json for better-auth, next-auth, @clerk/nextjs or lucia and 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 a getSession, it writes server/lib/flowpanel-session.ts built on the matching preset (withBetterAuth, withClerk, withNextAuth, withLucia) and points the config at it. A module that already exports getSession is left alone, and an unrecognised project still gets the closed getSession adapter. Connect your role mapping either way; async role lookups are supported. Use --dev-auth only to explicitly opt into an open development-only identity.
  • Stylesheet — admin.css imports @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 migrate vs npx flowpanel migrate, and so on).

When init stops

  • No Next.js in package.json — aborts, exit 1, with the install command for your package manager.
  • No Drizzle and no Prisma — aborts, exit 1, same treatment.
  • No TTY and no --yes, --dry-run or --json — init asks questions, so a non-interactive run without --yes exits 1 immediately: "init asks questions and this run has no interactive terminal. Re-run with --yes to accept the detected defaults." Pass --yes in 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 1 so the run is not mistaken for a success.
  • A dependency that installs but cannot be used is treated the same way. A @flowpanel/kit from another minor, or a project-local @flowpanel/cli from another minor than the one you ran, is listed in dependencies.failed with its own recovery command and exits 1. 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]
OptionDescriptionDefault
--dry-runPrint migrations that would be applied without running them—
--jsonEmit 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 migrate

Use --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]
OptionDescriptionDefault
--fixAuto-fix missing route files from templates—
--dry-runShow fixes without writing (use with --fix)—
--jsonEmit 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.ts exists
  • the four generated files exist: the API route, the SSE route, the seed migration, and the catch-all admin page
  • only one reachable @flowpanel/core installation 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 --noEmit passes

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]
ArgumentDescriptionStatus
<target>—required
[name]—optional
OptionDescriptionDefault
--forceOverwrite files if they already exist—
--dry-runPrint the filesystem plan without writing—
--jsonEmit 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 layout

It 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]
OptionDescriptionDefault
--port <port>Next.js dev port3000
--no-boardSkip 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 3000

Both 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>
ArgumentDescriptionStatus
<resource>—required
OptionDescriptionDefault
--table <expr>First argument expression (default: schema.<resource>)—
--kind <kind>Adapter kind: drizzle (default) or prismadrizzle
--dry-runPrint the filesystem plan without writing—
--jsonEmit 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.ts

It 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.