Skip to content

CLI

Terminal window
oven create my-app # scaffold a project
oven dev # run with a watcher
oven build # bundle and write a route manifest
oven start # run the build
oven db migrate # run schema and migration commands
oven worker # run background jobs
oven routes # print the route table
oven openapi # emit the OpenAPI document
oven doctor # check the project for common problems
Terminal window
oven create my-app --db sqlite --auth basic
Flag Values Default
--template minimal, api api
--db sqlite, postgres, none asked, or none
--auth basic, none asked, or none
--openapi --openapi / --no-openapi on
--yes skip every prompt —

--db sqlite scaffolds Drizzle over bun:sqlite — no server to run, no native module. The switch to Postgres is one import and one line in src/db.ts; every query you have written stays the same.

--auth basic adds working signup, login, refresh, logout, password change and reset, with the auth-basic tables re-exported from src/schema.ts so oven db generate picks them up. Mail defaults to the console driver, so the reset link is printed to your terminal and the whole flow works before you have configured a provider. Auth implies a database; asking for auth with --db none scaffolds SQLite and says so.

Every project also gets an AGENTS.md describing the conventions — route naming, return values instead of res.json(), defineRoute, native ORM queries — and the things never to do, because a model that has not read the docs otherwise reaches for Express habits.

Terminal window
oven create my-app --template api

Interactive on a TTY, flag-driven otherwise — a scaffolder that prompts inside CI hangs forever, which is a bad way to find out a pipeline is misconfigured. Pass --yes to skip prompts.

Template Contents
minimal one route, nothing else
api a small REST resource with validation and OpenAPI

It refuses to write into a non-empty directory. A directory holding only .git is fine, since git init comes first for a lot of people.

Terminal window
oven dev --port 4000

Runs the entry under a watcher. Restarts on change rather than patching modules in place: in-place reloading leaves a server bound to the port and brick state half-initialised from the previous version, and a clean restart takes single-digit milliseconds in Bun. The correctness is worth more than the milliseconds.

Signals are forwarded, so Ctrl-C reaches the app and its graceful shutdown actually runs.

Terminal window
oven build --outdir dist

Two steps, in this order:

  1. Write a route manifest — .oven/routes.ts, with a static import per route file.
  2. Bundle with Bun.build, targeting Bun.

The order matters: the bundler can only include route modules it can see imported by name.

Terminal window
oven routes
oven openapi > openapi.json
oven openapi --out openapi.json --title "My API" --api-version 1.0.0

Both import the app module — src/app.ts — not the entry. That is why the scaffold splits them: app.ts builds and exports the app, index.ts calls listen(). If inspection imported the entry, asking for a route table would bind a port.

With no --out, oven openapi writes only JSON to stdout, so it can be piped straight into a client generator.

Terminal window
oven db generate # write a migration from the current schema
oven db migrate # apply pending migrations
oven db push # push the schema straight to the database (development)
oven db studio # open the database browser
oven db drop # drop a generated migration

These delegate to whatever migration tool your project already uses, detected from its config file — drizzle.config.ts means drizzle-kit, prisma/schema.prisma means the Prisma CLI. Oven does not reimplement migration generation; a wrapper around it would drift from the real tool within a release.

What you get instead is one set of command names whatever adapter you chose, and translation where the tools disagree — oven db push runs drizzle-kit push in one project and prisma db push in another. A command with no equivalent says so rather than running something that looks similar.

generate, push, studio and drop shell out to drizzle-kit. migrate does not, on Drizzle projects: drizzle-kit’s SQLite migrator cannot use bun:sqlite and asks you to install better-sqlite3 or @libsql/client, which would mean the default Oven stack could generate a migration and then fail to apply it.

Drizzle ships Bun-native migrators, so oven db migrate uses those. It reads the same drizzle.config.ts drizzle-kit does, so the two cannot disagree about where migrations live.

Anything after -- is passed through unchanged:

Terminal window
oven db generate -- --name add_orders_table
Terminal window
oven worker # long-running; ctrl-c drains in-flight jobs
oven worker --concurrency 20
oven worker --once # drain what is queued and exit

Imports your app module, so the worker gets the same database, the same mail driver and the same job definitions the app has. A worker configured separately from its app is a worker that drifts from it — and the symptom is jobs dead-lettering as “no handler registered” after a deploy.

--once is for a cron container or a CI step: it drains what is queued and exits rather than idling. See queue.

Terminal window
oven doctor
✓ Bun version 1.2.23
✓ Entry src/index.ts
✓ App module src/app.ts
✓ Routes src/routes
! Environment .env.example present, .env missing
Copy it: cp .env.example .env
✓ Port 3000 is free

Every check that is not ok says what to do about it. A diagnostic that only reports a problem has done half the job.

Not a command, but the same class of problem — catching configuration mistakes at boot rather than in production:

import { env, defineEnv } from '@theoven/core'
const port = env.port('PORT', 3000) // parses, or throws naming PORT
const debug = env.bool('DEBUG', false) // DEBUG=false is false, not truthy

oven doctor checks that a .env and .env.example agree. See Environment variables for the full reader and for defineEnv.

oven db needs a migration toolchain in the project and oven worker needs the queue brick registered. Neither guesses: a project without one gets a sentence saying what is missing and what to add, rather than an error that looks like a typo you made.