The CLI
npm create ohne and every ohne command.
Installing ohne installs one binary, ohne:
ohne devruns a project while you work.ohne preparegenerates its types.ohne serveruns one backend in production.ohne syncupdates the database schema.
Your app and its layers can add commands of their own.
npm create ohne creates a project. Inside a project, npx runs that project's own copy of the binary:
npx ohne --helpThe scaffold also adds the everyday commands to package.json, so npm run dev and the other scripts there run without npx. npm test runs your tests.
Every command
--helpprints usage for any command, and bareohneprints the root help.ohne --versionprints the installed version.--cwdsets the project root, defaulting to the current directory. A command refuses to run without anohne.config.tsthere.- The
.envfile at that root is read before anything else. - Every built-in env var is also a flag, so
PORTis--port. The root help lists them under global options.
npx ohne serve api --port 8080 --host 0.0.0.0npm create ohne
npm create ohne [dir] scaffolds a new project: ohne.config.ts, package.json, tsconfig.json, and a .gitignore. In a terminal it asks for the location (when you did not pass dir), the project name, a website, the package manager, and git, then installs the dependencies.
Flags cover every prompt. With npm, they go after --. npm reads every flag before it as its own, so --yes, --name, and --pm there never reach the scaffold:
npm create ohne my-app -- --yes --gitpnpm hands the flags on as they are, so they need no --:
pnpm create ohne@latest my-app --yes --git--yes(-y) skips the prompts and the install, taking the defaults.--namesets the package name. Without it, the name comes from the directory, made into a valid npm package name when it is not one:My Appbecomesmy-app.--pmsets the package manager,npmorpnpm. Without it, the default is pnpm when you start the scaffold with pnpm, npm otherwise.--webadds a website,nuxtornext: a monorepo with your app inapps/backendand the website inapps/web. It needs pnpm, so--pm npmis refused.--gitinitializes a git repository.--force(-f) deletes everything in a non-empty directory, then scaffolds into it. Without it, the scaffold asks twice in a terminal before it deletes anything. Outside a terminal it refuses a non-empty directory.
Outside a terminal (CI, a script), npm create ohne behaves like --yes: no prompts, no install, and the target defaults to the current directory. Run npm install before the first npm run dev.
Installation takes a new project through its first run.
ohne dev
One command runs everything while you work:
npx ohne devIt starts the dashboard and the API, generates the types, and watches every layer in the stack. A linked workspace layer reloads like your own code. Only installed dependencies are skipped. On a change:
- API source or a message catalog - regenerates the types it affects and restarts the API.
- A dashboard file - reloads the open browsers. Nothing restarts.
ohne.config.ts- regenerates everything and restarts both servers..env- restarts both servers with the new values.
Each server is a child process, the same one serve runs in production. The API restarts as a fresh process, so no old module is left after a reload.
A project without a dashboard, such as an API another app's pages call, runs the API alone:
npx ohne dev --no-dashboardA codegen error, or a server that crashes or fails to start, prints its error and waits. The API starts again on your next change to API source, a message catalog, ohne.config.ts, or .env. The dashboard starts again on any change.
Each server binds its configured port, 9000 for the dashboard and 9001 for the API by default. When a port is already in use, dev warns and takes the next free one. PORT moves both: the dashboard takes it, and the API takes the next free port. Ports are picked once, when dev starts. A restart keeps them.
ohne prepare
prepare generates your project's types, then exits:
npx ohne prepareRun it after a pull, or in CI before tsc. The rest of the time you do not need it: dev and serve api generate the same types when they boot, and the scaffold adds prepare to package.json, where npm runs it after an install.
The output lands in .ohne/ by default: typed routes, middleware, message keys, database shapes, and the resolved config, with the Node types and the browser types in separate folders. Your tsconfig.json already includes them, and that is what makes queries and messages typed in the editor.
You never import from .ohne/ yourself. What it generates extends the types ohnejs exports, so import type { RoleName } from 'ohnejs' gives you your own roles. Server code has no import aliases, so use relative paths there. The dashboard's app/ alias is covered in pages.
Every run deletes what an earlier run generated, so no old file is left behind. It recognizes its own files by a banner at the top, which also records the ohne version that wrote them. A file of your own inside .ohne/ is never touched.
ohne serve
Production runs one process per backend:
npx ohne serve api
npx ohne serve dashboardserve api runs the boot files, regenerates the types, syncs the database schema, then listens. If the sync fails, it never serves. With SKIP_CODEGEN set it skips the regeneration, and warns when a different ohne version wrote the generated files.
serve dashboard serves the dashboard as a single-page app. For each request, it strips the types from the requested module and sends it as JavaScript. It finds the API through API_URL or dashboard.apiURL, and otherwise derives the address from the API's own config.
Both bind their configured host and port, or HOST and PORT when set. Deployment covers how the two processes fit together.
ohne sync
sync updates the database schema to match your collections, then exits. It is the same sync a server boot runs, without opening a port:
npx ohne sync--forceallows the destructive changes the sync would otherwise refuse.--dry-runruns the whole sync against the live database, then rolls it back. It exits non-zero where a real sync would refuse.
The destructive guard covers what the sync refuses.