⚒ boltforge

Boltforge.

A CLI that scaffolds a production-ready Node.js / Express / MongoDB backend — then keeps generating fully-wired features, one command at a time.

v0.1.0 MIT License Node ≥ 18 Express + Mongoose Zero lock-in — plain JS output

What is Boltforge? #

Boltforge is a code generator, not a framework. It writes a real Express + MongoDB project to disk — files you own, read, and edit like any hand-written project — and then keeps adding complete features to it on demand.

There is no runtime dependency on Boltforge. Once a project is generated you can uninstall the CLI and the project still runs, tests, and deploys normally. What Boltforge gives you is the first 80% of a backend that you'd otherwise write by hand every time: authentication, role/permission authorization, an admin area, error handling, validation, logging, security middleware, and a test harness — all wired together and consistent.

Generic engine

Every generator is discovered from disk at startup. Adding a new one is a new folder, not a CLI edit.

Nothing unused

Pick OTP and no password code is generated at all. Skip sockets and no socket server exists anywhere.

Never rewrites your files

Routes and permissions are discovered at startup. No generator edits a shared file to "register" anything.

All-or-nothing writes

Files are staged in a temp directory and moved in one atomic step. A failed generation leaves nothing behind.

Why Boltforge? #

Most starter templates hand you one snapshot and leave. Boltforge stays useful after day one.

ProblemHow Boltforge handles it
Boilerplate templates go stale the moment you add feature #2Feature generation is a first-class command, not a one-time scaffold
Starter kits ship code you never useOnly the strategy and add-ons you choose are generated
Generators that rewrite app.js and break your editsRoute and permission discovery at startup — no shared file is ever modified
A half-written feature after a crash mid-generationStaging plus a single atomic move; nothing lands unless everything does
Inconsistent structure once a team growsEvery feature comes from the same blueprint, so file #50 matches file #1
ScopeBoltforge targets REST APIs on Express + MongoDB (Mongoose). It does not generate frontends, GraphQL schemas, or SQL models today.

Installation #

Boltforge runs on Node 18 or newer. Install it globally so the boltforge command is available anywhere.

npm install -g boltforge-cli
yarn global add boltforge-cli
pnpm add -g boltforge-cli
# clone the repository, then:
cd boltforge-cli
npm install
npm link   # makes `boltforge` available globally

Verify the install:

boltforge-cli version
# 0.1.0
Node versionBoltforge uses dynamic import() for its prompt library and modern fs APIs. Node 18 is the minimum; Node 20+ is recommended.

Quick Start #

From nothing to a running API with a working feature, in about two minutes.

npm install -g boltforge-cli
boltforge-cli create my-api

What happens, step by step

  1. Boltforge asks you six questions (auth strategy, identifier, notifications, socket, Swagger, package manager).
  2. It builds a complete file plan in memory — nothing is written yet.
  3. It checks that no file it plans to write already exists.
  4. It writes every file into a temporary staging directory.
  5. Only once all files are staged successfully, it moves them into ./my-api in one step.
  6. It writes boltforge.config.json recording the choices you made.

Then run it

cd my-api
npm install
cp .env.example .env     # then fill in real values (Windows: copy .env.example .env)
npm run dev
Before first runSet a real MONGO_URI and replace the placeholder JWT_ACCESS_SECRET in .env. The app validates its environment at boot and exits with a clear message if something required is missing.

Add your first feature

boltforge-cli generate offer

That creates src/modules/offer/ with a model, validator, service, controller and routes, plus a unit test. Restart the server and /offers is live — no file was edited to register it.

More commands to try

boltforge-cli create my-api --auth otp --identifier email --yes
boltforge-cli generate offer
boltforge-cli generate controller users
boltforge-cli add socket
boltforge-cli add swagger
boltforge-cli doctor

Creating a Project #

boltforge-cli create scaffolds the base project once, at the start. Everything after that is generate and add.

boltforge-cli create <project-name> [options]

The base project always includes: the Express app, environment validation, the error architecture, security middleware, the auth module for your chosen strategy, the user module, the admin area (admin/admins and admin/users), route discovery, a logger, and a test harness.

Non-interactive (CI)

boltforge-cli create my-api --auth otp --identifier email --db mongodb --pm npm --yes

--yes skips every prompt and uses your flags plus defaults. Any flag you pass on its own is also respected in interactive mode — Boltforge only asks about what you did not specify.

Defaults under --yes--auth password, --identifier email, --db mongodb, --pm npm, and notifications / socket / Swagger all off.

Preview without writing

boltforge-cli create my-api --yes --dry-run

Prints the full list of files that would be created and exits. Nothing touches disk.

Interactive Create #

Run boltforge-cli create my-api with no flags and Boltforge walks you through the choices.

$ boltforge-cli create my-api
? Authentication:  (Use arrow keys)
❯ Password
  OTP (passwordless)
? Identifier:  Email
? Enable notifications (FCM)?  No
? Enable Socket.io?  No
? Enable Swagger UI?  No
? Package manager:  npm

✔ Project "my-api" created at /path/to/my-api (76 files)
info Next steps:
  → cd my-api
  → npm install
  → cp .env.example .env   # then fill in real secrets
  → npm run dev
PromptChoicesWhat it decides
AuthenticationPassword · OTPWhich auth module is generated. The other strategy's code is never written.
IdentifierEmail · Phone · Email + PhoneShape of the User model and which delivery provider is used.
Notifications (FCM)Yes · NoWhether the notification module and firebase-admin are included.
Socket.ioYes · NoWhether src/sockets/ is created now (you can always boltforge-cli add socket later).
Swagger UIYes · NoWhether /api-docs is mounted now.
Package managernpm · yarn · pnpmRecorded in config and used in the printed next-step commands.

Generate Commands #

Two forms, one engine: a shortcut for a whole feature, or an explicit type when you want a single file.

boltforge-cli generate <name>           # shortcut → generate feature <name>
boltforge-cli generate <type> <name>    # explicit type

The shortcut rule is simple: if generate receives one argument and it is not a known generator type, it is treated as a feature name. Passing a bare type with no name (boltforge-cli generate controller) is reported as a usage error rather than silently creating a feature called "controller".

Available types

typeProducesLocation
featuremodel + validator + service + controller + routes + unit testsrc/modules/<name>/
crudsame as feature, with all five CRUD operations implementedsrc/modules/<name>/
adminfeature files + a permissions file, permission-guarded routessrc/modules/admin/<name>/
controllerone controller filesrc/modules/<name>/
serviceone service filesrc/modules/<name>/
modelone Mongoose model filesrc/modules/<name>/
validatorone Joi validator filesrc/modules/<name>/
middlewareone custom middleware filesrc/middlewares/

Examples

$ boltforge-cli generate offer
✔ Generated feature "offer" (6 files)
  src/modules/offer/offer.model.js
  src/modules/offer/offer.validator.js
  src/modules/offer/offer.service.js
  src/modules/offer/offer.controller.js
  src/modules/offer/offer.routes.js
  tests/unit/offer.service.test.js
$ boltforge-cli generate crud product
# identical to: boltforge-cli generate feature product --crud
# routes created:
POST   /products
GET    /products          # paginated
GET    /products/:id
PATCH  /products/:id
DELETE /products/:id
$ boltforge-cli generate controller users
✔ Generated controller "users" (1 file)
  src/modules/users/users.controller.js

# useful when the rest of the module already exists
$ boltforge-cli generate admin reports
✔ Generated admin "reports" (7 files)
  src/modules/admin/reports/reports.model.js
  src/modules/admin/reports/reports.validator.js
  src/modules/admin/reports/reports.service.js
  src/modules/admin/reports/reports.controller.js
  src/modules/admin/reports/reports.routes.js
  src/modules/admin/reports/reports.permissions.js
  tests/unit/admin/reports.service.test.js

Flags

flagEffect
--crudImplement all five CRUD operations in the generated service, controller and routes.
--no-modelSkip the model file — useful when the feature reuses an existing model.
--no-testsSkip the generated unit test file.
--forceOverwrite files that already exist. Without it, a conflict stops the whole generation.
--dry-runPrint the planned files and exit without writing.
Run from inside a projectgenerate and add require a boltforge.config.json in the current directory. Outside a Boltforge project they stop immediately with a clear message.

Add-ons #

Optional capabilities you can bolt on at any time — not just at create.

boltforge-cli add socket
boltforge-cli add swagger
addonAddsResult
socketsrc/sockets/ — gateway, auth middleware, example handler, entry pointSocket.io attaches to the existing HTTP server on next start
swaggersrc/docs/swagger.js/api-docs serves every route already documented in your project
No file surgeryserver.js and app.js are written once at create with guarded optional hooks (fs.existsSync checks). Adding socket or Swagger later drops files into place — neither command edits an existing file.

Both add-ons support --force and --dry-run. If the add-on is already recorded in boltforge.config.json, Boltforge warns you before proceeding.

Doctor #

A read-only health check for the current project. Run it any time — especially before deploying.

$ boltforge-cli doctor
⚠ config says socket is enabled, but src/sockets/index.js is missing.
✖ JWT_ACCESS_SECRET is still the placeholder value from .env.example — replace it before running in production.
⚠ node_modules not found — run npm/yarn/pnpm install.

What it checks

  • Node version meets the minimum required by generated projects.
  • Core module directories recorded in config actually exist on disk.
  • Socket / Swagger flags in config match the files present.
  • .env exists and no longer contains the placeholder JWT secret.
  • package.json is present and dependencies are installed.
  • Whether the last generation finished or was interrupted.

Warnings are informational; errors set a non-zero exit code, so boltforge-cli doctor works as a CI gate. Pass --repair to resume an interrupted generation instead of regenerating from scratch.

Project Structure #

Click any file or folder to see what it does, what it depends on, and who uses it.

Select a file or folder from the tree to see its details.

The layout is feature-first: each feature owns one folder containing its model, validator, service, controller and routes. Generating a feature touches exactly one directory instead of scattering files across controllers/, services/ and models/.

Configuration #

Two files, two clearly separated jobs — they never overlap.

FileOwnsRead by
.envRuntime behaviour: database URI, JWT secret, SMTP/SMS credentials, portThe running application
boltforge.config.jsonWhat was generated and with which optionsThe boltforge CLI only

No secret ever appears in boltforge.config.json, and the application never reads it at runtime.

boltforge.config.json

{
  "cliVersion": "0.1.0",
  "createdAt": "2026-01-15T10:00:00.000Z",
  "auth": { "strategy": "otp", "identifier": "email" },
  "database": "mongodb",
  "notifications": { "enabled": true, "provider": "fcm" },
  "socket": false,
  "swagger": false,
  "packageManager": "npm",
  "modules": ["auth", "user", "admin"],
  "lastGeneration": null
}

Environment validation

src/config/env.js validates process.env against a Joi schema at boot. A missing or malformed required variable exits immediately with a readable message rather than failing mysteriously later.

NODE_ENV=development
PORT=3000
MONGO_URI=mongodb://127.0.0.1:27017/my-api
JWT_ACCESS_SECRET=replace-with-a-long-random-string
JWT_ACCESS_EXPIRES_IN=15m
STRICT_ROUTE_DISCOVERYOptional. Overrides the default route-discovery failure policy (fail-soft in development, fail-hard in production). See Route Discovery.

Authentication #

You pick one strategy at create time. Only that strategy's code is generated.

Both strategies share the same token foundation: a short-lived access JWT plus a rotating refresh token stored server-side. Neither strategy talks to an email or SMS vendor directly — they call a delivery abstraction instead.

Auth service
     ↓
VerificationService        // sendOtp / sendPasswordReset / sendVerification
     ↓
EmailProvider | SmsProvider  // "send this text to this address" — nothing more

Swapping SendGrid for SES, or Twilio for another gateway, means editing one provider file. The auth service is untouched because it never knew which vendor was in use.

Password Authentication #

Generated when you choose --auth password.

EndpointPurpose
POST /auth/registerCreate an account; password hashed with bcrypt (cost 12)
POST /auth/loginReturns an access token and a refresh token
POST /auth/refreshRotates the refresh token and issues a new access token
POST /auth/logoutRevokes the caller's refresh tokens
POST /auth/forgot-passwordIssues a single-use reset token via the delivery layer
POST /auth/reset-passwordConsumes the reset token and sets a new password
POST /auth/change-passwordRequires the current password; authenticated route
Account enumerationforgot-password responds identically whether or not the account exists, so the endpoint cannot be used to discover registered identifiers.

OTP Authentication #

Generated when you choose --auth otp. Passwordless: no password field is written to the User model at all.

POST /auth/request-otp             // 6-digit code, hashed, 5-minute TTL
POST /auth/verify-otp              // atomic single-use consume
     ├── known user   → { accessToken, refreshToken, sessionId: null }
     └── new identity → { accessToken: null, refreshToken: null, sessionId }
POST /auth/complete-registration   // consumes sessionId, creates the user

Why the two-step signup

Verifying a code proves the person controls that email or phone, but it does not yet mean an account exists. Rather than half-creating a user, Boltforge issues a short-lived sessionId (10 minutes) that carries only "this identifier was verified". Your registration endpoint exchanges it for a real account along with whatever extra profile fields you require.

Safeguards

  • Codes are stored hashed, never in plain text.
  • Consuming a code is an atomic find-and-mark, so two concurrent requests cannot both succeed.
  • Expired records are removed automatically by a MongoDB TTL index.
  • /auth/request-otp and /auth/verify-otp sit behind a stricter rate limiter than the rest of the API.

Email / Phone / Both #

--identifier decides the shape of the User model and which provider the delivery layer picks.

--identifierUser modelDelivery
emailidentifier + identifierType: 'email'EmailProvider
phoneidentifier + identifierType: 'phone'SmsProvider
bothidentifiers[] — array of { type, value, verified }Chosen per request from the identifier's type

The both mode is for products where a user may sign up with one channel and later attach the other — for example registering by email and adding a phone number for account recovery. The provider decision lives in one place (resolveProvider), never duplicated across the auth service.

Roles & Permissions #

Flat roles on the user, fine-grained permission strings alongside them.

roleMeaning
super_adminBypasses every authorization check. Never stores a permission list.
adminMay create assistant admins and grant permissions, including admins.*.
assistant_adminHolds exactly the permissions granted to them — never admins.*.
userDefault role for end users.

Permission strings

Permissions are namespaced <resource>.<action>, for example users.read, users.ban, offers.delete, notifications.send, admins.create.

The catalog assembles itselfsrc/constants/permissions.js recursively discovers every *.permissions.js file under src/modules/ and merges them at require time. Generating a new admin feature adds its permissions automatically — no shared catalog file is ever hand-edited.

The admins.* rule

The admins namespace is reserved for super_admin and admin. This is enforced inside admins.service.js at the moment permissions are granted — not merely documented — so even a direct service call cannot escalate an assistant admin.

Authorization #

One function decides every access question: AccessResolver.can(user, requirement).

authorize({ permissions: ['offers.delete'] })
authorize({ roles: ['admin', 'super_admin'] })
authorize({ roles: ['admin'], permissions: ['admins.create'] })  // BOTH required
RequirementRule
User has super_adminAllowed, always — checked first
roles onlyAllowed if the user has any of the listed roles
permissions onlyAllowed if the user has all of the listed permissions
roles + permissionsAllowed only if both conditions hold (AND)
Neither givenDenied
Read the last row carefullyCombining roles and permissions is an AND. authorize({ roles: ['admin'], permissions: ['admins.create'] }) requires the caller to be an admin and hold admins.create. If you want "admin OR anyone with the permission", specify the permission alone — super_admin still bypasses it.

Because the entire rule lives in access-resolver.js, moving to database-backed permissions later changes exactly that one file. The authorize() signature and every route calling it stay identical.

Admin Generator #

The admin area is feature-first too, so it grows without becoming a pile of loose files.

src/modules/admin/
├── admins/     # admin & assistant-admin management (created by `create`)
├── users/      # end-user management (created by `create`)
└── reports/    # ← boltforge-cli generate admin reports
boltforge-cli generate admin reports

This reuses the very same controller / service / model / validator / routes / test generators as a normal feature. A moduleDir parameter nests the output under admin/ and turns on the permission guard — there is no special case hard-coded into the CLI.

What you getDetail
Route prefix/admin/reports, written explicitly into the routes file
Permissionsreports.read, reports.create, reports.update, reports.delete
GuardingEvery route requires authentication plus the matching permission
Teststests/unit/admin/reports.service.test.js
Why permission-only guardsAdmin routes check permissions rather than roles + permissions. super_admin already bypasses everything, and gating on the permission alone is what lets an assistant admin actually use a feature they were granted.

Notifications / FCM #

Included when you pass --notifications. Firebase stays behind an interface.

NotificationService     // the only module the rest of your code imports
     ↓
NotificationProvider    // interface: sendToTokens / sendToTopic / (un)subscribeToTopic
     ↓
FCMProvider             // the only file that imports firebase-admin
NotificationService methodPurpose
sendToUser(userId, payload)Every registered device for one user
sendToUsers(userIds, payload)Batch send across many users
sendToTopic(topic, payload)Broadcast to an FCM topic
registerToken / removeTokenDevice token lifecycle
subscribeToTopic / unsubscribeFromTopicTopic membership for a user's devices

Multiple devices and dead tokens

Each device token is its own FcmToken document keyed by user, so one account can have many devices. When FCM reports a token as unregistered, NotificationService — not the provider — deletes it. Cleaning the database is an application concern, so it does not belong inside a vendor adapter.

await notificationService.sendToUser(userId, {
  notification: { title: 'Offer accepted', body: 'Your offer was accepted.' },
  data: { offerId: 'abc123' }
});
CredentialsSet FIREBASE_SERVICE_ACCOUNT_PATH in .env to point at your service-account JSON. Never commit that file.

Socket #

Real-time without coupling your business logic to Socket.io.

Service                                        Client
  │  eventBus.emit('offer.created', payload)      ▲
  ▼                                               │ io.emit(...)
Domain Event Bus  ──────────────────────►  SocketGateway
(plain EventEmitter)                       (only file importing socket.io)

Services emit domain events on a plain Node EventEmitter. The gateway listens and translates those into socket emissions. Nothing in src/modules/ ever imports socket.io, so removing the add-on later cannot break your services — the events simply have no listener.

FileRole
src/sockets/index.jsEntry point; attaches to the existing HTTP server
src/sockets/gateway.jsCreates the Socket.io server, bridges the event bus
src/sockets/socket.middleware.jsVerifies the same access JWT during the handshake
src/sockets/handlers/Client → service direction, like HTTP controllers

Socket.io shares the application's HTTP server rather than opening a second port, so it inherits the same TLS termination and deployment setup.

Swagger #

Order does not matter: features generated before you add Swagger still show up.

boltforge-cli generate offer     # writes @swagger JSDoc above the routes — always
boltforge-cli add swagger        # mounts /api-docs and reads ALL existing JSDoc

Every generated routes file carries @swagger annotations from the moment it is created. They are plain comments with zero runtime cost, so writing them unconditionally is free. Adding the add-on only turns on the reader — it never rewrites your existing routes.

Answer to the obvious questionGenerate ten features, then run boltforge-cli add swagger: all ten appear at /api-docs immediately. No regeneration needed.

Testing #

Jest, plus an in-memory MongoDB so tests need no running database.

npm test
npm run test:watch
PathContains
tests/unit/Service-level tests. Services take plain objects and never touch req/res, so they test cleanly in isolation.
tests/integration/Supertest against the Express app directly — no server needs to be listening.
tests/e2e/Reserved for your own end-to-end scenarios.
tests/setup.jsBoots mongodb-memory-server, clears collections between tests, tears down after.

generate feature, generate crud and generate admin each write a unit test alongside the feature by default. Pass --no-tests to skip it.

Error Handling #

One response contract, one place that decides it.

{
  "success": true,
  "message": "Offer created",
  "data": { "_id": "..." }
}
{
  "success": false,
  "message": "Validation failed",
  "error": {
    "code": "VALIDATION_ERROR",
    "details": { "email": "must be a valid email" }
  }
}
Error classstatuscode
ValidationError400VALIDATION_ERROR
AuthenticationError401AUTHENTICATION_ERROR
AuthorizationError403AUTHORIZATION_ERROR
NotFoundError404NOT_FOUND
ConflictError409CONFLICT
anything unexpected500INTERNAL_ERROR

Services throw; controllers do not catch. asyncHandler forwards rejections to the global error middleware, which calls classifyError() — the single function that maps any thrown value onto the contract above.

Mongoose errors are translated, not leaked

CastError becomes a 400, duplicate-key 11000 becomes a 409, and Mongoose validation errors become field-level details. Raw driver messages never reach the client.

No stack traces in responsesStacks are written to logs/error.log only. The client receives a message, a code, and sanitized details — never internals.

Security #

Defaults chosen so a fresh project is not insecure on day one.

ConcernHandling
HTTP headershelmet
Cross-origincors, explicitly configured
Parameter pollutionhpp
NoSQL injectionexpress-mongo-sanitize
Brute forceGeneral limiter, plus a stricter one on login / OTP endpoints
Password storagebcrypt, cost factor 12
Input validationJoi schemas at the route boundary
Secrets.env only, git-ignored; doctor flags placeholder values

Token theft detection

Refresh tokens belong to a family — the chain of tokens issued by successive rotations of one login session. Rotating a token retires the old one. If a retired token is presented again, that is a strong signal it was stolen: the entire family is revoked immediately and the event is written to logs/security.log.

OneTimeTokenStore   // OTP, password reset — used once, then dead
RefreshTokenStore   // issue / rotate / revokeFamily / reuse detection

The two stores are deliberately separate contracts. A reused OTP is just an error; a reused refresh token is a security event. Collapsing them into one interface would blur that distinction.

Generator Engine #

Every command runs through the same four pieces.

Registry

Scans src/generators/* at startup and registers whatever exports a type and a plan().

Template engine

Handlebars, deliberately logic-less. Decisions are resolved in the generator and passed in as booleans.

File plan

plan() is pure — it returns { targetPath, content }[] and performs no I/O.

File writer

Detects conflicts, stages every file, then commits with one atomic move.

A generator is one file

// src/generators/<type>/generator.js
module.exports = {
  type: 'repository',          // how it's invoked on the CLI
  describe: '...',
  plan(name, options, ctx) {   // pure: returns FilePlan[]
    return [{ targetPath, content }];
  }
};

Dropping that folder in is the whole installation step. generate.command.js, the argument parser and the registry contain no list of type names to update — they only ask the registry what exists.

Internal vs public typesSome registered generators (routes, test, permissions, project, socket, swagger) are building blocks used by blueprints rather than commands you type. The public generate types are: feature, crud, admin, controller, service, model, validator, middleware.

Feature Blueprint #

Blueprints compose existing generators. They never duplicate file-writing logic.

boltforge-cli generate offer
   ↓  not a known type → generate feature offer
   ↓  Feature Blueprint composes:
   model.plan() + validator.plan() + service.plan()
     + controller.plan() + routes.plan() + test.plan()
   ↓
   ONE combined FilePlan[] → ONE staging dir → ONE atomic move

Because the whole feature commits as a single unit, there is no state where the controller landed but the model did not. The admin blueprint works identically, adding permissions.plan() and passing moduleDir: 'admin'.

Route Discovery #

A feature becomes reachable because its file exists — not because something registered it.

At boot, src/routes/index.js recursively finds every *.routes.js anywhere under src/modules/, sorts them by path for deterministic ordering, and mounts each at the prefix it exports.

module.exports.prefix = '/offers';   // written explicitly by the generator
module.exports.router = router;

Recursion matters: admin sub-features live two levels deep at src/modules/admin/reports/. The prefix is written into the file rather than inferred, so pluralization is never guessed at runtime.

Failure policy

EnvironmentIf a route file fails to load
developmentFail-soft: log a clear warning naming the module, mount everything else, keep serving
productionFail-hard: abort startup — silently serving without a route is worse than a visibly failed deploy

Override explicitly with STRICT_ROUTE_DISCOVERY=true|false in .env.

Dependency Resolver #

Your package.json is computed from your choices, not copied from a fixed template.

Each blueprint declares its own dependencies in isolation. The resolver merges every selected set into one manifest, keyed by package name so a package can never be added twice. When two blueprints ask for different versions of the same package it keeps the higher range and prints a warning — it never resolves a conflict silently.

SelectionAdds
base (always)express, mongoose, dotenv, joi, winston, helmet, cors, hpp, express-rate-limit, express-mongo-sanitize, jsonwebtoken
--auth passwordbcrypt, nodemailer
--auth otpnodemailer, twilio
--notificationsfirebase-admin
--socketsocket.io
--swaggerswagger-jsdoc, swagger-ui-express
dev (always)jest, supertest, mongodb-memory-server
Nothing you did not ask forChoose OTP and bcrypt never appears in your manifest. Skip notifications and firebase-admin is never installed.

Lifecycle #

Every generation follows the same ordered pipeline.

validate  →  plan  →  conflict detection  →  [--dry-run stops here]
   →  write to temp staging directory        // project untouched so far
   →  atomic move into the project           // ← the single commit point
   →  update boltforge.config.json
   →  install dependencies  →  format  →  lint
Failure pointResult
Anywhere before the atomic moveProject is completely untouched. Rollback is free — nothing real happened yet.
After the atomic moveFiles stay. Status is recorded as partial and boltforge-cli doctor --repair resumes the remaining steps.
Why no rollback after the commit pointDeleting freshly written files could destroy edits you already made in the seconds after generation. Resuming the unfinished step is safer than undoing work that may no longer be only Boltforge's.

Dry Run #

See the plan before anything touches disk. Supported by create, generate and add.

$ boltforge-cli generate crud product --dry-run
info Dry run — 6 file(s) would be generated:
  src/modules/product/product.model.js
  src/modules/product/product.validator.js
  src/modules/product/product.service.js
  src/modules/product/product.controller.js
  src/modules/product/product.routes.js
  tests/unit/product.service.test.js

Dry run happens after conflict detection, so it also tells you whether the generation would collide with existing files.

Conflict Detection #

Boltforge never overwrites your work silently.

$ boltforge-cli generate offer   # when offer/ already exists
✖ Generation stopped — 6 file(s) already exist (use --force to overwrite):
  src/modules/offer/offer.model.js
  src/modules/offer/offer.validator.js
  ...

The check runs across the entire plan before any file is written. One existing file stops the whole generation — you never end up with a half-updated feature. Pass --force to overwrite deliberately.

--force is literalIt replaces the listed files with freshly generated ones. Any manual edits in those files are lost. Commit first.

Examples #

A jobs platform, from scratch

# passwordless auth by phone, with push notifications and realtime
boltforge-cli create jobs-api --auth otp --identifier phone --notifications --socket --pm pnpm --yes
cd jobs-api && pnpm install

# core domain
boltforge-cli generate crud job
boltforge-cli generate crud application
boltforge-cli generate company --no-model      # reuses an existing model

# admin side
boltforge-cli generate admin moderation
boltforge-cli generate admin analytics

# docs last — existing routes still show up
boltforge-cli add swagger
boltforge-cli doctor

Preview a risky regeneration

boltforge-cli generate crud job --dry-run       # check the collision list
git add -A && git commit -m "before regen"
boltforge-cli generate crud job --force

CI pipeline

npm install -g boltforge-cli
boltforge-cli create ci-api --auth password --identifier email --yes
cd ci-api && npm install
boltforge-cli doctor          # non-zero exit on errors
npm test

Troubleshooting #

“No boltforge.config.json found here”

generate and add must run from a project's root — the directory holding boltforge.config.json. Run cd my-api first, or boltforge-cli create if you have not scaffolded yet.

“<type> is a generator type, not a feature name”

You ran something like boltforge-cli generate controller with no name. Add the name: boltforge-cli generate controller users.

Generation stopped because files already exist

Expected behaviour — see Conflict Detection. Inspect the list, commit your work, then re-run with --force if you really want to overwrite.

Server exits at boot with an environment error

src/config/env.js validates required variables before anything starts. Copy .env.example to .env and fill in at least MONGO_URI and a real JWT_ACCESS_SECRET (16+ characters).

A route returns 404 even though the feature exists

Routes are discovered at startup — restart the server after generating. Then confirm the file is named *.routes.js, lives under src/modules/, and exports both router and prefix. In development a failed module logs a warning rather than crashing, so check the startup log.

403 on an admin route despite having the role

Check whether the route requires a permission as well. When a requirement lists both roles and permissions, both must be satisfied. Grant the specific permission to the account, or use super_admin, which bypasses all checks. See Authorization.

Refresh suddenly fails with “suspicious activity”

A refresh token that was already rotated was presented again, so the whole token family was revoked by design. Usually this means an old client copy replayed a stale token. The user must log in again; check logs/security.log for the event.

Swagger shows nothing at /api-docs

Confirm src/docs/swagger.js exists (boltforge-cli add swagger) and restart. Routes appear from their @swagger JSDoc comments, which generated route files already include.

Windows: “boltforge is not recognized”

The global npm bin directory is not on your PATH. Run npm config get prefix and add that folder to PATH, or invoke it directly via npx boltforge .... If you installed from source, make sure npm link completed without errors.

CLI Reference #

Every command and flag Boltforge implements today.

boltforge-cli create <project-name>

Scaffolds a new base project. Run once per project.

flagvaluesDescription
--authpassword | otpAuth strategy. Only the chosen one is generated. Default password under --yes.
--identifieremail | phone | bothUser identifier shape. Default email under --yes.
--dbmongodbDatabase. Only mongodb is supported; any other value is rejected.
--notificationsflagInclude the FCM notification module.
--socketflagInclude Socket.io immediately.
--swaggerflagInclude Swagger UI immediately.
--pmnpm | yarn | pnpmRecorded in config and used in printed next steps.
--forceflagOverwrite existing files.
--dry-runflagPrint the file plan and exit.
--yesflagSkip all prompts; use flags and defaults only.
boltforge-cli create my-api --auth otp --identifier email --db mongodb --pm npm --yes
boltforge-cli generate <name> · boltforge-cli generate <type> <name>

Generates a feature or a single file inside an existing project. One argument that is not a known type is treated as a feature name.

Types: feature, crud, admin, controller, service, model, validator, middleware

flagDescription
--crudImplement all five CRUD operations.
--no-modelSkip the model file.
--no-testsSkip the generated test file.
--forceOverwrite existing files.
--dry-runPrint the file plan and exit.
boltforge-cli generate offer
boltforge-cli generate controller users
boltforge-cli generate crud product --no-tests
boltforge-cli generate admin reports
boltforge-cli add <addon>

Adds an optional capability. Addons: socket, swagger. Supports --force and --dry-run.

boltforge-cli add socket
boltforge-cli add swagger --dry-run
boltforge-cli doctor

Diagnoses the current project. Errors produce a non-zero exit code.

flagDescription
--repairResume the last unfinished generation rather than starting over.
boltforge-cli doctor
boltforge-cli doctor --repair
boltforge-cli version · boltforge-cli help

boltforge-cli version prints the installed version; boltforge-cli --version and -V do the same. boltforge-cli help [command] lists all commands, or details one command's flags.

boltforge-cli version
boltforge-cli help generate

FAQ #

Is Boltforge a framework? Do I depend on it at runtime?

No. It generates plain Express/Mongoose JavaScript that you own. Uninstall the CLI and your project still runs, tests and deploys exactly the same.

Can I edit generated code?

Yes — that is the point. Boltforge never reads your files back or re-syncs them. The only time it touches an existing file is when you explicitly pass --force.

Can I switch auth strategy after creating the project?

Not with a command. The choice determines which module was generated, and Boltforge does not rewrite existing modules. For an early-stage project, regenerating is usually simpler; later on, adding the second strategy by hand against the same token stores is straightforward.

Does it support SQL, TypeScript, or GraphQL?

Not today. Output is JavaScript on Express + MongoDB. The generator engine is type-agnostic, so those are plausible future generators rather than architectural blockers.

How do I add my own generator?

Create src/generators/<type>/generator.js exporting type, describe and a pure plan(). The registry discovers it at startup — no CLI file needs editing. See Generator Engine.

Why are roles and permissions an AND when combined?

Because the restrictive reading is the safe default: authorize({ roles: ['admin'], permissions: ['admins.create'] }) means admin and holding that permission. For an either/or gate, require the permission alone — super_admin always bypasses.

Is generated code overwritten when I upgrade Boltforge?

No. Boltforge only writes when you run a command, and only to the paths in that command's plan. Upgrading the CLI affects future generations only.

Boltforge · MIT License · Documentation built as a single static page — no build step, no external requests.