The hery CLI
The hery CLI is the only thing in this project that reads a blueprint. It is a build-time tool, not a runtime dependency of the generated application.
| Command | What it does |
|---|---|
new <name> |
Scaffolds a fresh HeryJs project in its own directory. |
create:blueprint <Name> |
Writes a blueprint from prompts or defaults. |
generate <Name> |
Writes a full resource from that blueprint. |
migrate --name <name> |
Runs prisma migrate dev, then emits and applies any missing row-level policy. |
migrate:status |
Lists every migration as applied, pending, failed or missing. |
migrate:down |
Prints the SQL that reverts the last migration. |
migrate:fresh |
Drops everything and replays the history. Refuses in production. |
db:seed |
Runs prisma/seed.ts; --fresh resets first. |
db:diff |
Prints the SQL that would bring the database to prisma/schema/. |
db:branch <action> [name] |
Creates, drops or lists throwaway copies of the database. |
db:backup |
Dumps the database with pg_dump, keeping the newest N dumps. |
db:backups |
Lists the dumps of this database. |
db:restore <file> |
Restores a dump; refuses a database that has data without --force. |
install [modules...] |
Installs optional modules. |
uninstall <module> |
Removes a module and reverses what installing it did. |
make:mail <Name> |
Writes a Mailable into src/functional/<domain>. |
make:export <Name> |
Writes an Exportable into src/functional/<domain>. |
make:job <Name> |
Writes a queued job, its name and its retry policy. |
make:listener <Name> |
Writes an event listener. |
make:scheduled-task <Name> |
Writes a cron task that records its runs. |
module:list |
Lists the modules available to install. |
module:monitoring |
Scaffolds Prometheus, Grafana and Loki. |
search:reindex <Name> |
Rebuilds a resource’s search index from Postgres. |
openapi |
Emits an OpenAPI 3.1 document from every routed blueprint. |
openapi:client |
Emits a typed TypeScript client from the same blueprints. |
secrets:key |
Prints a new 32-byte key for SECRETS_KEYS. |
secrets:rotate |
Re-encrypts every encrypted column to the active key. |
expose:list |
Prints the catalog of @ExposeActions. |
expose:run <action> |
Runs one exposed action, --param name=value per field. |
up |
Checks that local dependencies are ready. |
doctor |
One command for environment, config and infra together. |
env pull |
Writes the resolved variables into .env. |
env run -- <command> |
Runs a command with the resolved variables injected. |
lint |
Scores the project against conventions eslint and the architecture linter do not reach. |
console |
Boots the app into a REPL. |
hosts |
Adds the local hostname to your hosts file. |
mcp:serve |
A read-only MCP server over stdio. |
| Command | What it does |
| —————————– | ————————————————————————————— |
new <name> |
Scaffolds a fresh HeryJs project in its own directory. |
create:blueprint <Name> |
Writes a blueprint from prompts or defaults. |
generate <Name> |
Writes a full resource from that blueprint. |
migrate --name <name> |
Runs prisma migrate dev, then emits and applies any missing row-level policy. |
install [modules...] |
Installs optional modules. |
uninstall <module> |
Removes a module and reverses what installing it did. |
make:mail <Name> |
Writes a Mailable into src/functional/<domain>. |
make:export <Name> |
Writes an Exportable into src/functional/<domain>. |
make:job <Name> |
Writes a queued job, its name and its retry policy. |
make:listener <Name> |
Writes an event listener. |
make:scheduled-task <Name> |
Writes a cron task that records its runs. |
make:policy <Name> |
Writes a capability policy, admin only until you say otherwise. |
make:seeder <Name> |
Writes a seeder exposed to expose:run and the mine, dev and test only. |
make:driver <module> <Name> |
Writes a driver and its token module from the module’s own contract. |
make:notification <Name> |
Writes a notification with one method per channel. |
make:webhook-handler <Name> |
Writes a handler for one webhook source. |
routes |
Prints every route with its capability and rate-limit bucket. |
module:list |
Lists the modules available to install. |
module:monitoring |
Scaffolds Prometheus, Grafana and Loki. |
search:reindex <Name> |
Rebuilds a resource’s search index from Postgres. |
openapi |
Emits an OpenAPI 3.1 document from every routed blueprint. |
openapi:client |
Emits a typed TypeScript client from the same blueprints. |
expose:list |
Prints the catalog of @ExposeActions. |
expose:run <action> |
Runs one exposed action, --param name=value per field. |
up |
Checks that local dependencies are ready. |
doctor |
One command for environment, config and infra together. |
env pull |
Writes the resolved variables into .env. |
env run -- <command> |
Runs a command with the resolved variables injected. |
lint |
Scores the project against conventions eslint and the architecture linter do not reach. |
console |
Boots the app into a REPL. |
hosts |
Adds the local hostname to your hosts file. |
mcp:serve |
A read-only MCP server over stdio. |
hery new <name>
Section titled “hery new <name>”Scaffolds a fresh HeryJs project in ./<name>: the CLI, every module’s authoring package, the kernel (technical/), the always-on DX tools (devtools/), the modules wired in by default, and a rewritten package.json. It then runs git init and commits the result, and prints the commands to take from there:
pnpm dlx heryjs new my-appcd my-appcp .env.example .envpnpm installpnpm hery up --startpnpm hery migrate --name initpnpm start:devheryjs is the published package and new is the only command it carries — the rest of the CLI reads the project around it, so it travels into the project rather than being installed globally. From inside my-app every command is pnpm hery, at the version that project was created with.
hery up --start rather than docker compose up -d: the compose services publish ports Docker assigns, so several projects can run at once, and up --start writes the ones it got back into .env. Bringing compose up by hand leaves .env pointing at ports nothing listens on.
hery migrate rather than prisma migrate dev on purpose, and the scaffolded project prints it that way: it wraps Prisma’s own migration and then emits the row-level security policy for every tenant-scoped table, so the database boundary is created with the tables instead of remembered afterwards.
Examples and the doc site are deliberately not copied — this is the framework’s own demo, not part of what ships.
The order that matters
Section titled “The order that matters”pnpm hery create:blueprint Task# edit blueprints/task.yaml if neededpnpm hery generate Taskpnpm hery migrate --name add_taskAfter this, Task is a normal NestJS module like any other. Nothing re-reads blueprints/task.yaml again — evolving the resource means editing the generated files directly, the same way you would for hand-written code.
hery create:blueprint <Name>
Section titled “hery create:blueprint <Name>”Walks through interactive prompts (fields, permissions, whether to paginate and with which page sizes, sortable fields, filterable fields) and writes a YAML blueprint to blueprints/<name>.yaml. Pagination defaults to off — --yes writes a blueprint whose search route returns every match.
--yesskips the prompts and takes sensible defaults — useful in scripts, or when trying the generator out.--all-optionsskips the prompts and writes a fully commented blueprint listing every field type, every permission preset and every other option the generator understands, mostly commented out. A quick way to see the whole menu before trimming it to what you need.
Every run also (re)writes blueprints/schema.json — a JSON Schema generated straight from the same zod schema hery generate parses a blueprint against, via zod’s own toJSONSchema — and stamps the new file with a # yaml-language-server: $schema=./schema.json modeline. An editor with the yaml-language-server extension picks that up on its own: autocompletion and inline validation for field types, permission presets and the rest, without opening a second file to check what is allowed.
hery generate <Name|path>
Section titled “hery generate <Name|path>”Reads the blueprint and writes ten files into src/functional/<name>/. See What gets generated for what each one owns.
The argument is a name resolved under blueprints/, or a path to a YAML file if you would rather keep a blueprint next to whatever it produced:
pnpm hery generate Taskpnpm hery generate examples/blog-post.yamlIt also patches prisma/schema/ (the new model in app.prisma, its inverse relation on User in _hery/core.prisma), prisma.client.ts (adding the model to the tenant-scoped set) and audit-log.ts (adding it to the audited set), then prints the two manual steps it deliberately does not take for you: importing the module into src/app.module.ts, and running the migration.
--forceoverwrites an existing resource directory. Without it,generaterefuses rather than clobbering code you own.--graphql,--mcp,--live,--streameach add one more file, wiring the resource into the corresponding module. They require that module to be installed; the flag does not check.
hery migrate --name <migration-name>
Section titled “hery migrate --name <migration-name>”Runs prisma migrate dev, then does the second half generate deliberately left undone: it reads the schema’s /// @hery.tenantScoped annotations, writes the ENABLE ROW LEVEL SECURITY migration for any tenant-scoped table that has no policy yet, and applies it. Writing that migration by hand is how the teams tables spent a month scoped by the extension with nothing behind them.
hery install [modules...]
Section titled “hery install [modules...]”Installs optional modules à la carte or, with --all, the full package. hery module:list prints what is available. See The module system.
pnpm hery install storage mailpnpm hery install --allhery make:mail <Name> and hery make:export <Name>
Section titled “hery make:mail <Name> and hery make:export <Name>”Where a module has a recurring per-use-case object, it gets a make: command. Both write one class into src/functional/<domain>/, next to the code that uses it, and both refuse to overwrite an existing file unless you pass --force:
pnpm hery make:mail WelcomeMail # src/functional/welcome/welcome-mail.tspnpm hery make:export TaskListExport -d task # src/functional/task/task-list-export.tsThe domain is derived from the name with a trailing mail or export stripped, and --domain overrides it. What gets generated implements a small interface — Mailable, Exportable — and never picks a driver, reads config, or names a format: that is the registry’s job, and keeping it out is what lets the same class survive a transport change. See Modules and drivers.
hery make:job, make:listener, make:scheduled-task
Section titled “hery make:job, make:listener, make:scheduled-task”The same shape, for the three concern types the kernel already runs and nothing scaffolded:
pnpm hery make:job SendDigestJob # src/functional/send-digest/send-digest.tspnpm hery make:listener WelcomeListener # src/functional/welcome/welcome.tspnpm hery make:scheduled-task ExpireTokens -d authThe kind’s suffix is stripped before anything is derived, so make:job SendDigestJob and make:job SendDigest produce the same file — SEND_DIGEST_JOB,
SendDigestProcessor, and a domain of send-digest.
Each carries the part a copy-paste quietly drops:
- a job ships a retry policy. Written by hand it inherits BullMQ’s defaults — one attempt, no backoff — and a job that fails once is simply gone.
- a listener carries a stable
name, because a queued job travels with it: renaming it while jobs are enqueued strands them. - a scheduled task goes through
ScheduledTaskStore, so a task that stops firing is visible rather than merely silent.
Each also prints what is left to do — the payload, the event class, the cron expression, and the module to register it in. Generated code is yours from the moment it lands; these fill in the decisions, not the work.
make:policy, make:seeder, make:driver, make:notification, make:webhook-handler
Section titled “make:policy, make:seeder, make:driver, make:notification, make:webhook-handler”Five more scaffolds on the same pattern: one file next to the code that uses it, --force to overwrite, next steps printed.
pnpm hery make:policy ManageInvoices # src/functional/manage-invoices/manage-invoices.policy.tspnpm hery make:seeder InvoiceSeeder # an @ExposeAction, so `expose:run invoice.seed` workspnpm hery make:driver mail postmark # src/modules/mail/postmark-mail.driver.ts and .module.tspnpm hery make:notification OrderShipped # toMail, toSms, toPushpnpm hery make:webhook-handler StripeWebhookHandler- a policy is
adminAbilityunder a name, so a route cannot be guarded by a rule nobody wrote down. - a seeder is bounded (
max: 500) and restricted todevelopmentandtest, the same two rulesagency.seedcarries. - a driver is read from
src/technical/<module>/<module>-driver.tsrather than from a table in the CLI: every method of the contract becomes a stub that rejects with “not implemented”, so an unfinished driver fails loudly instead of pretending to send. It also writes the@Global()module that provides thedriverToken(<module>, <name>)the registry resolves. - a notification declares what it says per channel; delete the channels it has nothing to say on.
- a webhook handler names its
sourceand registers itself with the webhook processor, which hands it every event for that source inside the event’s tenant, before the admin notification goes out. A handler that throws fails the job, so the event is retried rather than marked processed.
hery routes
Section titled “hery routes”Boots the application and prints what Nest’s router actually has wired:
METHOD PATH CAPABILITY RATE LIMITGET /blog-posts canViewAnyPost readPOST /webhooks/:id public (signed by its sender) exempt (sender is trusted)--unguarded keeps only the routes with no capability or no rate-limit bucket, --json prints the rows. A route that is deliberately public or unthrottled shows its stated reason instead of a blank.
There is deliberately no module:add: hery install already does that.
The database workflow
Section titled “The database workflow”migrate and migrate:down were the whole of it. The rest of what Laravel’s
migrate:* and db:* commands do now exists under the same names:
pnpm hery migrate:status # applied / pending / failed / missing, per migrationpnpm hery migrate:fresh --seed # drop everything, replay, seedpnpm hery db:seed --fresh # the same reset, seeding alwayspnpm hery db:diff # SQL from the live database to prisma/schema/pnpm hery db:branch create risky # a copy of the database to try a migration onpnpm hery db:backup # a pg_dump in backups/, newest 7 keptpnpm hery db:restore latest # refuses a database that already has datamigrate:status reads _prisma_migrations rather than guessing from the disk,
which is how it can say missing for a migration that was applied and then
deleted from the repository, and failed for one that stopped halfway. --check
exits 1 unless everything is applied, for CI.
migrate:fresh and db:seed --fresh refuse when NODE_ENV is production, and
--force does not change that. They also refuse a database that is not on this
machine unless you pass --force: a command that deletes a database has to be
harder to run by accident than on purpose.
db:diff is read-only. It prints what the next migrate would do; --check exits
1 when the database and the schema differ.
db:branch is Supabase branching for a local database. create <name> runs
CREATE DATABASE ... TEMPLATE, so the copy has the schema and the rows, and
prints the DATABASE_URL to export. Postgres cannot copy a database somebody is
connected to, so a source with open connections is reported as that, not as a raw
Postgres error. drop <name> can only drop a database this command made: the name
is always <database>_branch_<name>, and a branch name is limited to lowercase
letters, digits and underscores because it ends up in DDL, where parameters are not
allowed.
hery uninstall <module>
Section titled “hery uninstall <module>”Removes the module’s own npm dependencies (pnpm remove) and prints the rest of what to clean up by hand — the runtime files it copied in and the kernel files it patched. It only automates what is safe to automate outright: your own edits to those files since installing are exactly what “own your code” says the tool must never guess about and silently revert. Run hery module:list to see what’s installed. See The module system.
hery search:reindex <Name>
Section titled “hery search:reindex <Name>”Rebuilds a resource’s search index from what is actually in Postgres — the source of truth stays the database, the index is a derived cache that can always be thrown away and rebuilt. Run it after installing a search engine module on a resource that already has data, or any time the index and the database might have drifted. See Full-text search.
hery expose:list and hery expose:run
Section titled “hery expose:list and hery expose:run”The two CLI halves of the exposition registry: expose:list prints every @ExposeAction the app declares, expose:run runs one outside a request.
pnpm hery expose:run prune.run --param prune.run.model=BlogPostThere is no signed-in user at a terminal, so the capability check is skipped where the HTTP route enforces it — trust comes from having a shell on the machine, the same reasoning console --tenant relies on. The environment filter and the audit entry still apply. See Exposing an action to the mine.
hery module:monitoring
Section titled “hery module:monitoring”Scaffolds Prometheus, Grafana and Loki as an opt-in local compose stack, wired to scrape the app’s /metrics. It writes the compose file and a Prometheus config, then prints the command to start them — it does not start anything itself.
/metrics is caller-authenticated like every other route, so the scrape carries an admin API key: the config reads it from monitoring/api-key, which the command creates with a placeholder. Mint a key with POST /api-keys, put it in that file on its own line, and keep the file out of version control. Until then every scrape gets a 401.
Despite the name it is a plain command, not a registry module: it does not appear in module:list and install --all does not cover it.
hery up
Section titled “hery up”Checks that the things the app needs are actually reachable — Postgres, Valkey, and whether Prisma migrations are up to date — and exits non-zero if any of them is not, with a hint for each:
✔ PostgreSQL (localhost:32768)✔ Valkey (localhost:32769)✘ Prisma migrations — run "pnpm hery migrate --name <name>"Reachable here means the service answered as itself, not that a socket opened: Postgres is asked for a select 1 and Valkey for a PING. Since the compose files let Docker pick the host port, recreating the containers can hand Postgres the port .env still has under REDIS_URL and the other way round — two services up, two URLs pointing at the wrong one, and a connection check would call that healthy. This one says which service failed to answer and tells you to run --start:
✘ PostgreSQL (localhost:32769) — something is listening there, but it does not answer as PostgreSQL — was the container recreated on a new port? run "pnpm hery up --start"--start brings the compose services up first and then does something more interesting: it reads back the ports Docker actually assigned and writes them into .env. The compose files publish container ports without fixing a host port, so several projects can run side by side without colliding, and hery up --start is what reconciles that with your configuration. It resolves DATABASE_URL and REDIS_URL, plus ELASTICSEARCH_URL or MEILISEARCH_URL if the matching search module is installed.
hery down and hery up --lift
Section titled “hery down and hery up --lift”hery down puts the running application in maintenance mode: every request answers 503 with a Retry-After until hery up --lift ends it. --secret lets requests carrying a secret through, --retry <seconds> sets the Retry-After, and --message <text> sets what the 503 says. The switch lives in Redis, so it reaches every instance at once.
hery doctor
Section titled “hery doctor”Everything hery up checks, plus the two config files a broken value in either would otherwise crash the whole CLI on: environment variables (parsed against the same zod schema env.ts builds at startup) and hery.config.ts (loaded for real, then checked for one thing loading it does not catch on its own — that search.default actually names a key in search.engines).
✔ Environment variables✔ hery.config.ts✔ PostgreSQL (localhost:32769)✔ Valkey (localhost:32768)✔ Prisma migrationsBoth config checks go through require() inside a try/catch rather than a normal import, on purpose: env.ts and hery-config.ts both validate at import time and throw if the result is invalid, which is exactly the failure doctor exists to turn into a diagnosis instead of a crash.
hery env pull and hery env run
Section titled “hery env pull and hery env run”The environment schema in env.ts is split into two halves. The server half (env) never leaves the process; the public half (publicEnv) is the short list of variables the admin dashboard’s browser bundle is allowed to read, declared once so a typo in a name fails at startup instead of quietly shipping http://localhost:3000 to production. no-server-env-in-client (part of hery lint) enforces the other direction: a client-side read of anything not in that list is a critical violation, because Astro inlines whatever it reads into the bundle it sends to the browser.
Where the values themselves come from is a separate question, handled by a small contract rather than by wiring a provider into the app:
interface EnvSource { name: string; load(): Promise<Record<string, string>>;}The only implementation shipped is .env itself. env pull resolves the configured source and writes the result into .env; env run -- <command> resolves it and runs a command with the values injected, without ever writing them to disk:
pnpm hery env pullpnpm hery env run -- pnpm start:devThis is deliberately a CLI-only concern. env.ts reads process.env once, synchronously, at import time, and the rest of technical/ depends on that as a plain value — making it async to accommodate a remote secrets manager (Infisical, Doppler, Vault, or one a team builds itself) would contaminate every module that reads env for a need that exists only at startup. Implementing EnvSource against a real provider and pointing dotEnvSource at it instead is enough to swap one in; nothing downstream of .env changes.
hery lint
Section titled “hery lint”Scores the project out of 100 against conventions eslint and the architecture linter do not reach — forbidden patterns (any, a controller importing Prisma directly) and the shape a generated resource is supposed to keep (every file hery generate writes, present). A fresh install is never going to start at 100 for a codebase that predates the rule, so a baseline file grandfathers what already exists:
pnpm hery lint --write-baseline # snapshot today's violations as known debtpnpm hery lint --min-score 100 # fail only on anything newThe baseline (.hery/lint-baseline.json by default, override with --baseline <path>) keys each grandfathered violation by the rule and a hash of the exact file content it was recorded against — never by path. Rename the file and the debt follows it; touch even one line and the file’s violations count as new again. That is deliberate: a baseline immune to edits would let someone change a violating line for an unrelated reason and walk away having silently re-endorsed it.
This framework’s own baseline is empty, and CI runs --min-score 100 against it. There is no known debt left to grandfather here, so the next violation introduced fails the build on the commit that introduces it.
--format json prints { score, grandfathered, violations } instead of the text report, for a script or an agent to act on.
hery console
Section titled “hery console”Boots the real application into a REPL with the DI container and the tenant-scoped Prisma client. --tenant <id> picks the tenant the whole session runs inside. See Developer tooling.
hery hosts
Section titled “hery hosts”Adds heryjs.local (or whatever HERYJS_DOMAIN is set to) to your system hosts file, pointing at 127.0.0.1, so the app is reachable by name instead of by port. It shows the exact line it will add and asks before touching anything, then elevates — sudo on Unix, an admin prompt on Windows. If the entry is already there it says so and does nothing.
hery mcp:serve
Section titled “hery mcp:serve”Starts a read-only MCP server over stdio, for editors and agents that want to introspect what exists in the project. Two tools:
list_resources— the resources actually generated undersrc/functional/.describe_resource(name)— that resource’s routes (HTTP method, path, the capability guarding it) and its fields, read straight from the real*.controller.tsandprisma/schema/.
It never reads a blueprint file. A blueprint is a one-time input to generate, not a live source of truth — the generated code is.
This is the read-only surface, needs no running app and no credentials. For an agent that should be able to use the application — with a real session and real capability checks — install the MCP module instead.