Skip to content

Runtime Commands ​

Start, serve, and manage your application's runtime processes — API server, queue workers, scheduler, UI, and developer tools.

bash
npm run maker <command> [options]
bash
pnpm maker <command> [options]
bash
yarn maker <command> [options]
bash
bun maker <command> [options]

Development Server ​

dev ​

Start the full development stack — API server, UI, queue worker (if Redis enabled), and optional dev tools — all in parallel with a single command.

What starts:

ProcessStarted whenCommand
API serverAlwaysserve --src (hot-reload via tsx watch)
UIUI != "false"Vite dev server on port 5173
Queue workerREDIS != "false"queue:work --queue=default,mail --src
Optional tools--view / --with flagsMailDev, Redis Commander, Drizzle Studio

All child processes are tracked. If any required process exits unexpectedly, the entire stack shuts down.

bash
npm run maker dev
bash
pnpm maker dev
bash
yarn maker dev
bash
bun maker dev

Additional tools ​

bash
npm run maker dev -- --view=redis,maildev,studio
npm run maker dev -- --with-redis-view --with-maildev --with-db-studio
bash
pnpm maker dev --view=redis,maildev,studio
pnpm maker dev --with-redis-view --with-maildev --with-db-studio
bash
yarn maker dev --view=redis,maildev,studio
yarn maker dev --with-redis-view --with-maildev --with-db-studio
bash
bun maker dev --view=redis,maildev,studio
bun maker dev --with-redis-view --with-maildev --with-db-studio
ToolFlagWhat it provides
Redis Commanderredis / --with-redis-viewWeb UI at port 1369 to inspect Redis keys
MailDevmaildev / --with-maildevSMTP server at port 1089 + web UI at port 1080 to view sent emails
Drizzle Studiostudio / --with-db-studioWeb UI at https://local.drizzle.studio for database browsing

ui:dev ​

Start only the UI dev server (alias: admin:dev). Useful when you want to run the API separately.

bash
npm run maker ui:dev
bash
pnpm maker ui:dev
bash
yarn maker ui:dev
bash
bun maker ui:dev

Production Server ​

serve ​

Start the HTTP API server.

Flags:

FlagDefaultDescription
--prod—Run compiled dist/src/framework/server.js
--runtime <name>nodeRuntime: node or bun
--watch—Watch for file changes (source mode)
--src—Force source mode even if dist/ exists
bash
npm run maker serve
npm run maker serve -- --prod
npm run maker serve -- --prod --runtime=node
npm run maker serve -- --runtime=bun
npm run maker serve -- --watch
npm run maker serve -- --src
bash
pnpm maker serve
pnpm maker serve --prod
pnpm maker serve --prod --runtime=node
pnpm maker serve --runtime=bun
pnpm maker serve --watch
pnpm maker serve --src
bash
yarn maker serve
yarn maker serve --prod
yarn maker serve --prod --runtime=node
yarn maker serve --runtime=bun
yarn maker serve --watch
yarn maker serve --src
bash
bun maker serve
bun maker serve --prod
bun maker serve --prod --runtime=node
bun maker serve --runtime=bun
bun maker serve --watch
bun maker serve --src

Queue ​

queue:work ​

Start a BullMQ queue worker that processes jobs from named queues.

FlagDefaultDescription
--queue <names>defaultComma-separated queue names (e.g. default,mail,maintenance)
--prod—Run from compiled dist/ instead of source
--runtime <name>nodeRuntime: node or bun
--src—Force source mode even if dist/ exists
bash
npm run maker queue:work
npm run maker queue:work -- --queue=mail
npm run maker queue:work -- --queue=default,mail,maintenance
npm run maker queue:work -- --prod
npm run maker queue:work -- --queue=default,mail --prod --runtime=node
bash
pnpm maker queue:work
pnpm maker queue:work --queue=mail
pnpm maker queue:work --queue=default,mail,maintenance
pnpm maker queue:work --prod
pnpm maker queue:work --queue=default,mail --prod --runtime=node
bash
yarn maker queue:work
yarn maker queue:work --queue=mail
yarn maker queue:work --queue=default,mail,maintenance
yarn maker queue:work --prod
yarn maker queue:work --queue=default,mail --prod --runtime=node
bash
bun maker queue:work
bun maker queue:work --queue=mail
bun maker queue:work --queue=default,mail,maintenance
bun maker queue:work --prod
bun maker queue:work --queue=default,mail --prod --runtime=node

queue:clear ​

Delete all queue-related Redis keys. Useful for resetting stale job state during development.

bash
npm run maker queue:clear
bash
pnpm maker queue:clear
bash
yarn maker queue:clear
bash
bun maker queue:clear

Scheduler ​

schedule:work ​

Start the cron scheduler worker. Executes scheduled tasks defined with defineSchedule() on their configured intervals.

bash
npm run maker schedule:work
npm run maker schedule:work -- --prod
bash
pnpm maker schedule:work
pnpm maker schedule:work --prod
bash
yarn maker schedule:work
yarn maker schedule:work --prod
bash
bun maker schedule:work
bun maker schedule:work --prod

Testing ​

test ​

Run Vitest backend tests once and exit.

Pass anything after the command straight through to Vitest, so all of Vitest's own options work here — --coverage, --reporter=verbose, --bail, and so on.

bash
npm run maker test
npm run maker test -- posts
npm run maker test -- --reporter=verbose
bash
pnpm maker test
pnpm maker test posts
pnpm maker test --reporter=verbose
bash
yarn maker test
yarn maker test posts
yarn maker test --reporter=verbose
bash
bun maker test
bun maker test posts
bun maker test --reporter=verbose

Filtering is positional, not a flag

To run part of the suite, name it — maker test posts matches test files whose path contains posts. There is no --filter option; Vitest has no such flag and errors with Unknown option \--filter`` if you try one.

The npm form needs one separator, not two

npm run maker test -- posts is right. npm run maker test -- -- posts also works, but for a confusing reason: npm consumes the first -- and forwards the second, which arrives as Vitest's argument separator and reads as a filter matching nothing. Because that quietly runs the whole suite instead of the part you asked for, it looks like it worked.

pnpm, yarn and bun need no separator at all.

test:watch ​

Run Vitest in watch mode — re-runs when a file changes.

bash
npm run maker test:watch
npm run maker test:watch -- posts
bash
pnpm maker test:watch
pnpm maker test:watch posts
bash
yarn maker test:watch
yarn maker test:watch posts
bash
bun maker test:watch
bun maker test:watch posts

This one keeps running

test runs once and exits; test:watch stays open until you stop it, which is why test:run is the one to reach for when you want an answer and an exit code.

test:coverage ​

Run Vitest once with a code coverage report.

bash
npm run maker test:coverage
npm run maker test:coverage -- auth
bash
pnpm maker test:coverage
pnpm maker test:coverage auth
bash
yarn maker test:coverage
yarn maker test:coverage auth
bash
bun maker test:coverage
bun maker test:coverage auth

The report is written to coverage/ — a text summary in the terminal plus an HTML report you can open at coverage/index.html. That directory is gitignored, so it never shows up in a commit.

This reports; it does not judge

A new project has no coverage threshold, so this always succeeds and simply tells you the number. That is deliberate: the percentage includes the whole framework, not just your code, so a threshold calibrated for the framework itself would fail your project on its first run with every test passing.

See Testing for how to set one that is worth having once you have written enough tests.

test:ui ​

Run Vitest backend tests in the visual UI mode.

bash
npm run maker test:ui
bash
pnpm maker test:ui
bash
yarn maker test:ui
bash
bun maker test:ui

Dev Tools ​

maildev:view ​

Start MailDev — a combined SMTP server and web email viewer. Catches all outgoing emails during development.

ServicePort
SMTP1089
Web UI1080
bash
npm run maker maildev:view
bash
pnpm maker maildev:view
bash
yarn maker maildev:view
bash
bun maker maildev:view

redis:view ​

Start Redis Commander — a web UI to browse and manage Redis keys.

ServicePort
Web UI1369

If Redis is not reachable, the view does not fail silently. redis:view first probes the configured REDIS_URL and, when nothing answers (or REDIS=false in .env), it serves a Redis Commander Unavailable page on port 1369 naming the Redis URL it tried, instead of leaving the browser to report "unable to connect".

This page is specific to Redis Commander. It is not the queue dashboard: queue and job state live on the app's own bull-board route at /queues, which shows its own Queue Dashboard Unavailable page when Redis is down. The two tools are separate — Redis Commander browses raw Redis keys, the bull-board shows BullMQ queue state.

bash
npm run maker redis:view
bash
pnpm maker redis:view
bash
yarn maker redis:view
bash
bun maker redis:view

vite:cache:clear ​

Clear Vite caches. Removes .vite directories from node_modules, src/resources, and other locations.

bash
npm run maker vite:cache:clear
bash
pnpm maker vite:cache:clear
bash
yarn maker vite:cache:clear
bash
bun maker vite:cache:clear

Custom Commands ​

Your project can add its own maker commands. The files you own are maker/commands.mjs (plain JavaScript) or maker/commands.ts (TypeScript) in the project root.

It is deliberately outside src/. Anything under src/framework/ is copied into the published package, so an edit there is lost on the next framework update, and src/modules/ is swept by the route, job and seeder globs.

maker:init ​

Creates maker/commands.mjs with a working example. It will not overwrite an existing file unless you pass --force. It always creates the .mjs — to write your commands in TypeScript instead, create maker/commands.ts yourself and delete the .mjs, as described under TypeScript commands.

bash
npm run maker maker:init
bash
pnpm maker maker:init
bash
yarn maker maker:init
bash
bun maker maker:init

Writing them ​

program is the same commander object the built-in commands are built with, so .command(), .description(), .option() and .action() behave exactly as they do everywhere else in the maker CLI.

js
// maker/commands.mjs
import { sendInvoice } from "../src/invoices/send.js";

export default function register(program) {
  program
    .command("invoice:send <customer>")
    .description("Send an invoice to a customer")
    .option("--dry-run", "Print what would be sent")
    .action(async (customer, options) => {
      await sendInvoice(customer, { dryRun: options.dryRun });
    });
}
bash
maker invoice:send acme --dry-run
maker --help          # your commands appear alongside the built-ins

Export a default function, or a named register export — either is accepted.

TypeScript commands ​

Name the file maker/commands.ts instead of maker/commands.mjs and the loader reads it through tsx, with your project's tsconfig.json applied — so @/ imports and your path aliases work exactly as they do inside src/:

ts
// maker/commands.ts
import { sendInvoice } from "@/invoices/send.js";

export default function register(program) {
  program
    .command("invoice:send <customer>")
    .description("Send an invoice to a customer")
    .option("--dry-run", "Print what would be sent")
    .action(async (customer, options) => {
      await sendInvoice(customer, { dryRun: options.dryRun });
    });
}

Everything above still applies — same exports, same commander object, same error behaviour, with maker/commands.ts named in the messages instead.

  • maker/commands.mjs never touches tsx. The tsx hooks are registered only when the .ts file is the one being loaded, once per process. Plain-JavaScript projects and production installs without devDependencies are therefore unaffected; a project that uses maker/commands.ts needs tsx in its devDependencies (the scaffold includes it).
  • Actions can import TypeScript too. The hooks stay registered for the life of the process, so an action handler can await import("@/modules/…/something.js") at dispatch time.
  • Your editor type-checks this file — the build does not. maker/ stays out of the root tsconfig's include: adding it would change the common source directory and relocate every emitted file under dist/. Instead the scaffold ships maker/tsconfig.json — extends the root config, noEmit, rootDir: ".." — and that is the config your editor picks up for anything under maker/, so @/ paths, node types and imports from src/ all resolve with no red squiggles. build:backend's tsc -p tsconfig.json never reads it, so dist/ is untouched; at run time tsx transpiles without type-checking, and biome still lints the file.

If your project predates 4.2.2, create that file yourself — it is the whole thing:

json
{
  "extends": "../tsconfig.json",
  "compilerOptions": { "noEmit": true, "rootDir": ".." },
  "include": ["./**/*.ts"]
}

Four things that will bite you ​

Absent is silent. If neither maker/commands.mjs nor maker/commands.ts exists, nothing happens and nothing is printed. That is the common case, so it has to be free.

One file at a time. If both exist, maker/commands.mjs wins — it is the incumbent, and a project already using it must not change behaviour because a .ts file appeared. The .ts is not left silently dead: maker prints which file it ignored and why.

text
maker/commands.ts is ignored because maker/commands.mjs exists. Keep one - delete the file you are not using.

Broken is loud. If the file exists but cannot be parsed, throws while loading, or exports the wrong shape, maker stops with an error naming the file — maker/commands.mjs or maker/commands.ts — and exits non-zero. It is never swallowed — a developer's commands silently not existing is far worse than a message. Every failure names the file:

text
Could not load maker/commands.mjs: Unexpected end of input
maker/commands.mjs must export a default function (program, args) => void. Found: undefined.

A broken file blocks every maker command

This includes the framework's own. Until the file parses, module:make and the rest are unavailable too — the CLI cannot know which of your commands were supposed to work.

To get out, fix the file, or delete it. Deleting leaves nothing behind: nothing caches a registration and no framework update refers to it, so maker returns immediately to exactly the state it was in before maker maker:init.

Framework names win. Commander refuses to register a command whose name is already taken, so a project cannot shadow a built-in it did not write. The error names both the file and the name, so rename yours:

text
Could not register commands from maker/commands.mjs: cannot add command
'module:list' as already have command 'module:list'

Stubs are not overridable

A custom command does not get a custom stub. module:make-* reads from src/framework/maker-cli/stubs/, which is framework-owned and replaced on every update. If you need your own template, write the file yourself from your command — that is the honest trade, and it keeps the generated file under your control.

Finding a command ​

maker --help shows the nexwire wordmark, then groups the commands by what you are trying to do, so you can answer a question instead of reading a flat list of every name.

When you will not see the wordmark

The wordmark is decoration, so it stays out of the way rather than insisting on itself:

  • A terminal narrower than 41 columns gets plain nexwire maker text instead. Art that overflows wraps into an unreadable mess.
  • Piped or redirected output gets plain text. maker --help > help.txt produces a file a human can read, not a file that opens with ASCII art.
  • Per-command help gets no wordmark at all. maker module:make --help is for checking a flag, not for branding.
text
Getting started
  dev                Start API + Vue 3 UI plus optional workers/tools
  serve              Start HTTP server (dist first if built, else src)

Generators - global
  middleware:make    Generate a middleware file in src/middlewares

Generators - modules
  module:make        Create one or more modules with facade, schema, service...
  module:make-model  Generate a model file for an existing module

Modules - manage
  module:list        List all discovered modules
  module:delete      Move a module directory to storage trash (soft delete)

Database
  db:migrate         Generate then run Drizzle migrations

The full set of sections is Getting started, Generators - global, Generators - modules, Modules - manage, Database, Queue and scheduler, Deploy, Testing, and Your commands.

Generators - global is not the same as Generators - modules

middleware:make writes to src/middlewares and applies to the whole app. module:make-middleware writes inside a single module and applies only to it. They are near-identical names doing different things, so they are deliberately listed in different sections.

The listing shows command names and descriptions only. For the options and arguments of one command:

bash
npm run maker <command> --help
bash
pnpm maker <command> --help
bash
yarn maker <command> --help
bash
bun maker <command> --help

maker help <command> does the same thing and still works.

Why your own commands appear under "Your commands"

Commands you register in maker/commands.mjs or maker/commands.ts are listed beside maker:init, not mixed in with the built-ins, so it is obvious which commands you added.

Summary ​

CommandDevelopmentProduction
devFull stack (API + UI + queue + tools)—
serveAPI with watchAPI without watch
queue:workQueue worker from sourceQueue worker from dist
schedule:workScheduler from sourceScheduler from dist
ui:devVite dev server—
testRun tests once—
test:watchRun tests in watch mode—
test:coverageRun tests with coverage—
test:uiRun tests in UI mode—
queue:clearDevelopment cleanup—
maildev:viewEmail testing—
redis:viewRedis inspection—
vite:cache:clearCache cleanup—
maker:initCreate maker/commands.mjs—

Released under the MIT License.