typebase-io-cli generate-server
Generate server files locally for custom hosting or inspection.
Last updated on
The generate-server command builds your server files locally instead of deploying them. This is useful when you want to:
- Self-host your server on your own infrastructure
- Inspect the generated code to understand what Typebase produces
- Customize the deployment process beyond what
npx typebase-io-cli deployoffers
To run your server on your own machine, use start instead. It builds, installs, applies your schema changes, and runs the
server from one command, rebuilding and restarting on every change. generate-server produces an artifact and stops there: it installs nothing,
never touches a database, and the server it writes has to be started and restarted by you.
Reach for generate-server when the generated files themselves are the point — reading them, hosting them, or building on them — or when you need an adapter or output format other than the ones a local run picks for itself.
Quick start
npx typebase-io-cli generate-serverThis generates a complete, runnable server in the typebase/_server/ directory (by default).
It regenerates _generated/ first (the same work codegen does), then type-checks your project against those fresh types, then builds.
Embedded servers
By default generate-server writes a standalone server: one that owns a process, creates its own HTTP listener, and starts it. --embedded writes an
embedded server instead — the same actions, database, and auth, emitted without a bootstrap, for an application that already owns its process to mount:
npx typebase-io-cli generate-server --embeddedThis writes source and a typebase-server.json marker into typebase/_handler/ (by default), then prints what your application has to provide before it can
mount them.
Set { "server": { "embedded": true } } in typebase.json to make this the default. Set it to false or remove it to generate a standalone server again;
--embedded can override a configured false.
--port conflicts with --embedded, and is also rejected when embedded generation is enabled in configuration: an embedded server never listens, so nothing
would use it. A port stored in typebase.json is ignored rather than rejected, so one configuration file can still serve every command.
start always builds a standalone server, whatever server.embedded says.
What an embedded build emits
_handler/
src/
server.ts # Router, RPC handler, auth wiring, and the mount
_generated/
server.ts # Action builder and middleware setup
actions/ # Your action files (copied and processed)
db/ # Your schema, relations and migrations (if applicable)
auth.ts # Your auth config (if applicable)
env.ts # Validated environment variables (if applicable)
publisher.ts # Publisher runtime config (if applicable)
storage.ts # Storage runtime config (if applicable)
typebase-server.json # Adapter, mode, CLI version, dependencies, env keyssrc/server.ts exports the same three names for every adapter, so switching adapters doesn't touch your integration code:
| Export | What it is |
|---|---|
typebaseHandler | The value you mount. Its shape follows the adapter: a Fastify plugin, a Hono app, a Connect-style Node middleware, or a fetch handler for Bun, Deno and Cloudflare. |
router | The router the handler serves, so you can build on it beyond what the mount provides. |
auth | The better-auth instance, when your project has an auth.ts, so your own routes can read the session. |
There is no index.ts, and no package.json, tsconfig.json, .env, or package-manager config file: an embedded server is a fragment of somebody else's
application rather than an application of its own. TypeScript output uses .js import specifiers for its own relative imports, so it compiles under an ordinary
application tsconfig.json under both nodenext and bundler resolution.
--output esm and --output cjs are available too, but pick the one your application already uses. A standalone server declares its own "type" in the
package.json it ships; an embedded server has no manifest, so its .js files are interpreted using your application's "type" — esm output in a
"type": "commonjs" host, or cjs output in a "type": "module" host, fails at import.
What your application provides
Three things an embedded server deliberately leaves to its host:
- Dependencies. It ships no dependency tree of its own. A nested
package.jsoninside your application would mean a second copy ofdrizzle-orm,better-auth, orfastifyloaded in one process, where aninstanceofcheck across the two copies fails and the error surfaces nowhere near its cause. The build tells you what to install in your own package instead. - Environment. It never loads a
.envfile, and no.envis written into the output directory, so mounting it cannot change environment your application already configured — and a generated fragment inside your source tree never becomes a place secrets accumulate.src/env.tsis still emitted and still validates on import, so the values have to be inprocess.envbefore your host importsserver.ts. - Cross-origin policy. The generated code sets no CORS headers. Same-origin browser requests need none. For cross-origin browser clients, configure your host's CORS middleware before the Typebase routes so it handles preflight requests and response headers for both actions and auth. The generator omits its own CORS plugin rather than install a second policy alongside yours.
The first build reports all three after its success message:
Embedded server requirements
Install missing or incompatible dependencies in the host package:
npm install '@orpc/server@1.14.3' 'better-auth@1.6.11' 'drizzle-orm@1.0.0-beta.22'
npm install -D 'typescript@5.9.3'
Required Environment keys: DATABASE_URL, BETTER_AUTH_SECRET
Load these in your host before importing the embedded server.
Cross-origin headers are your host application's responsibility. Configure its CORS policy for your clients.
Mount from your project root (adjust the import for your entry point):
import { typebaseHandler } from "./typebase/_handler/src/server.js";
// In your existing Express application:
app.use(typebaseHandler);
Dependency and environment details: typebase/_handler/typebase-server.jsonThe install command uses the package manager your project uses, and lists only what your package.json doesn't already declare. A dependency you have at a
version the generated server doesn't support is warned about and included. Everything the report says is also in typebase-server.json, so an install step can
read it instead of you.
The list is what a standalone build would install, plus anything your own code inside typebase/ imports that the host doesn't declare. In a workspace that
second group matters: when your typebase/ directory belongs to a different package from the one mounting the server, the imports your actions rely on are
declared somewhere the host can't resolve them from.
--watch, rebuilds stay quiet so a long session stays readable.Mounting
One line per adapter, against the default typebase/_handler/ output. See HTTP adapters for choosing one.
import express from 'express';
import { typebaseHandler } from './typebase/_handler/src/server.js';
const app = express();
app.use(typebaseHandler);It calls next() when the request wasn't for Typebase, so your other routes still run. Called without a next — createServer(typebaseHandler) — it answers
404 instead.
import Fastify from 'fastify';
import { typebaseHandler } from './typebase/_handler/src/server.js';
const app = Fastify();
await app.register(typebaseHandler);Typebase's catch-all content type parser is registered inside the plugin, and Fastify scopes parsers to the plugin they belong to, so your own routes keep the body parsing they had.
import { Hono } from 'hono';
import { typebaseHandler } from './typebase/_handler/src/server.js';
const app = new Hono();
app.route('/', typebaseHandler);import { typebaseHandler } from './typebase/_handler/src/server.js';
Bun.serve({ fetch: typebaseHandler });import { typebaseHandler } from './typebase/_handler/src/server.js';
Deno.serve(typebaseHandler);import { typebaseHandler } from './typebase/_handler/src/server.js';
export default { fetch: typebaseHandler };In a CommonJS application built with --output cjs, the import becomes const { typebaseHandler } = require('./typebase/_handler/src/server.js') and the
Cloudflare mount becomes module.exports = { fetch: typebaseHandler }.
Serving paths
An embedded server is mounted wherever you like, so the paths it serves are configurable, and independently of each other:
| Flag | Serves | Default |
|---|---|---|
--actions-path <path> | Your actions | /rpc |
--auth-path <path> | Auth | /api/auth |
--storage-path <path> | Local storage files, with --local-storage only | /storage |
npx typebase-io-cli generate-server --embedded --actions-path /typebase/rpc --auth-path /typebase/authBoth are also server.actionsPath and server.authPath in typebase.json. The defaults are the paths a standalone
server serves, so a build that sets neither is unchanged.
Both options are embedded-only. Passing either with a standalone build is an error, because a standalone server always serves the paths it was built with and
nothing would use the value. A value inherited from typebase.json is ignored instead, for the same reason a configured port is.
Generation refuses when the auth path would swallow the actions path — --auth-path /typebase together with --actions-path /typebase/rpc, for instance. Auth
is matched first, so every action request would be answered by auth and never reach your actions. The storage path is checked the same way: it can't sit
under the auth path, and it can't contain the actions path. Keeping the two paths independent, rather than deriving both
from one prefix, is what lets you move either one without the other picking up a duplicated segment.
A custom actions path changes the URL your client points at. See the server URL. The auth path is also pushed
into the generated auth config as better-auth's basePath; see the auth path for what happens
when your own auth.ts already sets one.
Output formats
You can choose between three output formats:
TypeScript (default)
npx typebase-io-cli generate-server --output tsGenerates TypeScript source files. You'll need to compile them yourself before running.
ESM (ECMAScript Modules)
npx typebase-io-cli generate-server --output esmGenerates JavaScript files using import/export syntax. Ready to run with Node.js (with "type": "module") or Deno.
CommonJS
npx typebase-io-cli generate-server --output cjsGenerates JavaScript files using require/module.exports syntax. Ready to run with Node.js without "type": "module".
HTTP adapters
Typebase supports multiple HTTP adapters. The adapter determines how your server handles incoming HTTP requests:
| Adapter | Description | Best for |
|---|---|---|
node | Node.js built-in http.createServer | Self-hosting with Node.js (default) |
bun | Bun's built-in HTTP server | Self-hosting with Bun |
hono | Hono framework | Vercel, general-purpose |
fastify | Fastify framework | High-performance Node.js |
deno | Deno's built-in Deno.serve | Deno Deploy |
cloudflare | Cloudflare Workers fetch handler | Cloudflare Workers |
npx typebase-io-cli generate-server --adapter hononpx typebase-io-cli generate-server --adapter nodenpx typebase-io-cli generate-server --adapter fastifyOptions
| Flag | Description | Default |
|---|---|---|
--output <type> | Output format: ts, esm, or cjs | ts |
--adapter <adapter> | HTTP adapter: node, bun, hono, fastify, deno, cloudflare | node |
--embedded | Generate source for your existing application to mount | false |
--out-dir <path> | Output directory for generated files | _server, or _handler when embedded |
--actions-path <path> | Path an embedded server serves your actions at (embedded only) | /rpc |
--auth-path <path> | Path an embedded server serves auth at (embedded only) | /api/auth |
--local-storage | Keep a Vercel or Cloudflare storage's files on disk and serve them from the server | false |
--storage-path <path> | Path an embedded server serves local storage at (embedded and --local-storage only) | /storage |
--watch | Rebuild on every change inside your typebase/ directory | false |
--command <command> | Command to run in the generated server after it is generated | none |
--port <number> | Port the generated server listens on (standalone only) | 8080 |
Default configuration
You can set defaults in your typebase.json so you don't have to pass flags every time:
{
"server": {
"output": "esm",
"adapter": "hono",
"outDir": "_server",
"port": 8080
}
}See the Configuration page for all available options.
What gets generated
A standalone server directory contains:
_server/
src/
index.ts # Bootstrap: loads env and starts listening
server.ts # Router, RPC handler, auth wiring, and the mount
_generated/
server.ts # Action builder and middleware setup
actions/ # Your action files (copied and processed)
db/ # Your schema, relations and migrations (if applicable)
auth.ts # Your auth config (if applicable)
env.ts # Validated environment variables (if applicable)
publisher.ts # Publisher runtime config (if applicable)
storage.ts # Storage runtime config (if applicable)
.local-storage/ # Local storage files (with --local-storage, once the server has stored one)
package.json # Dependencies for the server
tsconfig.json # TypeScript config
typebase-server.json # Adapter, mode, CLI version, dependencies, env keys
pnpm-workspace.yaml # Package-manager config (name depends on your package manager)src/server.ts builds everything a host needs to serve requests and src/index.ts starts it listening. Both modes generate the first; only a standalone build
generates the second, which is why an embedded server can't drift from the runnable one, the runnable one is built on top of it.
A package-manager config file sits next to package.json when your package manager needs one: .yarnrc.yml for yarn berry, bunfig.toml for bun, and
pnpm-workspace.yaml for pnpm. The pnpm one makes the generated server a workspace root in its own right. Without it, installing inside a generated server
that sits under a pnpm workspace walks up to your workspace root, installs your own projects instead, and leaves the generated server with no dependencies
while still reporting success. It also carries allowBuilds, which is where pnpm 11 reads the permission for esbuild to run its install script; pnpm 10
reads that from the pnpm field in the generated package.json instead. npm, yarn classic, and a package manager we cannot identify get no extra file.
src/publisher.ts is emitted when the project has a publisher.ts; it creates the configured provider with the runtime resources it needs. For the db provider, that is the generated database client.
.env in a standalone server is filled in from your project-root .env, with the keys the server validates: DATABASE_URL,
BETTER_AUTH_SECRET, your storage credentials, and whatever you declared in env.ts. DATABASE_URL and the
storage keys come from their _DEV versions when there are any, so a server you run locally reaches the dev branch and the dev buckets. Keys already in the file are never overwritten, and nothing else from your project .env (provider
tokens especially) is copied. This is a generate-server convenience only; deploy syncs variables to the provider instead and
never puts a .env in the bundle. An embedded server gets no .env either: your application loads the environment it runs in.
src/db/migrations/ is copied across when your project uses migrations, for every output format, and the drizzle config
written next to it points its out at them. The generated server is self-contained: you can apply the migrations it shipped with using standard
drizzle tooling. The server never reads them itself — migrations are applied by the CLI, from your machine or from CI. A project in push mode gets no
migrations folder.
src/env.ts is emitted whenever your project has an env.ts, a db/schema.ts, an auth.ts, or a storage.ts that needs credentials — anything that means the server has variables to validate. In a standalone server, dotenv is added to package.json and dotenv/config imported by index.ts, only when that env module exists and the adapter is not cloudflare, which reads its values from Worker bindings rather than process.env. A server with nothing to read gets neither. An embedded server never loads environment files at all, since it has no bootstrap and mutating its host's environment is not its business.
Storage
src/storage.ts is emitted when the project has a storage.ts. It creates your declared storage with what the provider needs to run, and files-sdk
is added to the server's dependencies.
By default that is your real provider. A vercel or cloudflare storage reads its credentials from the environment,
under the same names a deployed server uses, so a self-hosted server is configured exactly like a deployed one. A filesystem storage needs nothing and
keeps its files in its own root.
--local-storage
--local-storage swaps a vercel or cloudflare provider for local storage, the same one a local run
uses: files on disk, served and signed by the generated server itself. It needs no credentials, so it reads no storage variables.
npx typebase-io-cli generate-server --local-storageIt is a flag rather than a fallback on purpose. A server generated without it that is missing a storage variable fails at boot, instead of quietly writing production uploads to a disk that may not survive the next deploy.
Files are kept in .local-storage, resolved against the directory the server runs from. For a standalone server started from typebase/_server/, that's
typebase/_server/.local-storage/, which regeneration keeps alongside .env and node_modules. The URL signing secret is created there
on first use.
A standalone server serves the files at /storage on its own port. An embedded server serves them at --storage-path, or server.storagePath in
typebase.json, and builds its URLs as paths relative to your application's origin. The route is part of
typebaseHandler, like auth, so mounting is unchanged. A server built without --local-storage has no storage route at all.
--local-storage does nothing for a project without a storage.ts, or whose storage.ts declares filesystem, and says so. It can't be combined with
--adapter cloudflare, since a Worker has no disk to keep files on, and --storage-path without --local-storage is an error, because nothing would
serve the path.
Watch mode
generate-server is a snapshot, so every edit normally means running it again. --watch does that for you:
npx typebase-io-cli generate-server --watchIt builds once, then rebuilds whenever anything inside your typebase/ directory changes. Press x or Ctrl+C to stop.
The first build reports each step. Every rebuild after that is a single Regenerating... line, so a long session stays
readable. Errors are still printed in full.
Changes to _generated/ and to the output directory are ignored. Generation writes to both, so watching them would
make every build trigger the next one.
If you save again while a build is running, that build is cancelled and a new one starts, so the output always reflects your latest sources rather than a half-finished older run. A build is only cancellable up to the point where it starts replacing the output directory; the final copy always runs to completion, so the directory is never left half written.
A failed build, a type error most often, does not stop the watcher. The error is printed and the next change triggers another attempt.
Watch mode rebuilds the files in the output directory. A server you already started from them keeps running the old code until you restart it. See Work locally.
Watch mode is a long-running command. If you drive the CLI from a script or an agent, run it in the background or with a timeout, and stop it with
SIGINT rather than x when the output is piped.
Running the server on every rebuild
--command runs a command inside the generated server directory once generation finishes. Combined with --watch, it restarts that command on every
rebuild, so what is running is always the latest build:
npx typebase-io-cli generate-server --watch --command "pnpm start"The examples on this page assume a standalone server, where that directory is a package with a start script. --command is unchanged for an
embedded server, including the directory it runs in, but that directory holds no package.json, so a package script like pnpm start
has nothing to run there. Pass something that doesn't depend on the output directory being a package, or pass nothing and let your own application's watcher
pick up the regenerated source.
If what you want is a server running on your machine, start is the shorter path: it does the install for you, applies schema
changes on every pass, keeps the build out of your project tree, and starts the server without a package script in between — which is also why it
never prints the ELIFECYCLE line below.
The one-time setup is installing dependencies in the generated server, since the command runs there:
npx typebase-io-cli generate-servercd typebase/_server
pnpm installThen leave the watcher running. Editing anything in typebase/ rebuilds the server and restarts your command against it.
Regenerating replaces typebase/_server/, but node_modules and .env are kept — which is why installing once is enough, and why your local
environment variables survive.
The command is run through a shell, so it behaves the way it does when typed. Stopping with x or Ctrl+C stops the command too, including the server
your package script started rather than just the script itself.
A rebuild that fails leaves the running command alone, so a type error does not take your server down. A command that exits — because it crashed, or because it was never long-running — does not stop the watcher either; the next change runs it again.
Without --watch, the command runs once after generating and the CLI waits for it, still stopping it on x or Ctrl+C.
The command does not get the terminal's input. That keeps x and Ctrl+C working while it runs, so an interactive command is not what this flag is
for. Its output goes straight to your terminal as usual.
ELIFECYCLE Command failed. after every reload
Expected, and not a failure. Stopping the command signals it, and pnpm reports any script that ends on a signal as a failed lifecycle — the same line
you get from pressing Ctrl+C during a plain pnpm start. The rebuild and the restart both worked.
Pass --silent to keep pnpm quiet about it:
npx typebase-io-cli generate-server --watch --command "pnpm --silent start"Running the server directly, without a package script in between, avoids it too:
npx typebase-io-cli generate-server --watch --command "node src/index.js"Regeneration
Each run rebuilds the server from scratch and replaces the previous output directory — files whose source was deleted or renamed don't linger. Three exceptions survive regeneration because they belong to you, not the generator: .env, node_modules, and the .local-storage files of a server generated with --local-storage. Install dependencies and configure your env once; re-runs keep them.
If a generation fails, the previous output is left untouched, so a working server is never destroyed by a failed rebuild.
As a safety measure, the CLI only replaces a directory it recognizes as its own: one that is empty, doesn't exist yet, or carries the typebase-server.json marker a previous build wrote. Pointing --out-dir (or server.outDir) at any other non-empty directory aborts with an error instead of overwriting it. A directory generated before markers existed is still recognized by its @typebase-io/server package.json, so upgrading doesn't make your next build refuse to run.
That check matters most for an embedded server, which has no package.json to be recognized by and is often pointed at a directory inside your own source tree, where replacing the wrong one is expensive.
Running the generated server
This section is about a standalone server. An embedded server has no entry point and no dependencies of its own: your application starts it by mounting it.
After generating the server, you can run it like any Node.js project:
cd typebase/_server
# Install dependencies
npm install
# Run (if TypeScript output)
npx tsx src/index.ts
# Run (if ESM output)
node src/index.js
# Run (if CJS output)
node src/index.jsThe server starts on port 8080 by default. Override it with --port or the server.port field in typebase.json.