Work Locally
Run your Typebase server on your own machine during development.
Last updated on
There are two ways to work on your backend. You can deploy to dev once, point your frontend at the deployed URL, and redeploy whenever the backend
changes. Or you can run the server on your own machine, where saving a file is all it takes for the running server to be the one you just wrote.
The second is a local run:
npx typebase-io-cli startWhich one should you use?
Deploying to dev is less to run and a good fit while your work is mostly on the frontend, or when your backend changes once a day rather than once a
minute.
Reach for a local run when:
- You are iterating on server code. A save rebuilds, re-applies your schema, and restarts the server, so there is no redeploy between writing an action and calling it.
- You want a debugger. It is a Node process on your machine, so
--command "node --inspect src/index.ts"attaches one. - You want to see every request. The local server prints a line for each one with the action it called, the input it sent, and the status, and prints the stack of any error under it.
- You are offline, or the network is slow. Nothing is uploaded and nothing is fetched from a provider.
- You do not want to touch a shared database. A local run defaults to a Postgres you run yourself rather than your dev branch.
- You do not want test files in real buckets. A local run keeps storage on your own disk unless you ask for your dev buckets.
A local run never deploys. It does not pick a provider, upload anything, write provider environment variables, provision a database, or create a bucket. It connects to the database you give it and runs the server on your machine.
Starting a local run
Run it from your project root. There is no directory to change into and nothing to install first:
npx typebase-io-cli startEach pass builds your server, installs its dependencies, brings your database in step with your schema, and starts it. Then it watches typebase/ and
does all four again on every change. Press x or Ctrl+C to stop, and the server stops with it.
The generated server is built into a server cache outside your project, so a local run never adds a file for you to
review, commit, or ignore. typebase/_server/ is left alone.
Restarting the command redoes everything, which is the way out of any state you do not trust. The start reference covers every
option.
Giving it a database
A local run applies your schema changes, so a project with a db/schema.ts has to say which database to apply them to. Set DATABASE_URL_LOCAL to a
Postgres you run yourself:
DATABASE_URL_LOCAL=postgres://localhost:5432/mydbThat is the default because a local run pushes schema changes, and defaulting to your dev branch would push them somewhere your team shares. To reach a target instead, say so:
npx typebase-io-cli start --dev-database| Flag | Database |
|---|---|
| none | DATABASE_URL_LOCAL in your project-root .env |
--database-url <url> | The connection string you pass |
--dev-database | DATABASE_URL_DEV, your dev branch |
--prod-database | DATABASE_URL, your production branch |
Each one reads exactly one key, and none falls back to another. The run names the one it picked before it touches it:
ℹ Using the database from DATABASE_URL_LOCAL.If the key you chose is empty, the run stops and tells you which one to set. A project with no db/schema.ts skips all of this and needs no database at
all.
Storage
A project with a storage.ts on vercel or cloudflare needs no buckets, credentials, or network for a local run. Its files are kept in
local storage: the same buckets, on disk in the server cache, so they survive every
restart and never appear in your project.
The local server serves and signs their URLs itself, under /storage. An upload flow built on signedUploadUrl works in the browser against
http://127.0.0.1:8080/storage/... exactly as it will against the real provider, signed URLs expire when they should, and a tampered or expired one is
refused.
To reproduce something with real files, run against your dev or production buckets:
npx typebase-io-cli start --dev-storageThat reads the credentials storage sync dev wrote to your project-root .env, so run the sync once first if you have never
deployed. The storage flags are independent of the database flags: --dev-database with local storage, or a local database with --dev-storage, are
both fine.
Editing your schema
Every pass brings the database in step before the server restarts, so the server you are talking to matches the schema you just saved. In push mode it pushes; in migrations mode it applies your pending migrations.
Two things differ from a deploy, both because a watch loop cannot stop and wait:
- Drift warns instead of refusing. A schema edit you have not recorded as a migration keeps the loop
running, so you can carry on deciding what the migration should say.
deploystill refuses, which is right for production. - The database is only touched when
typebase/db/changed. Editing an action rebuilds and restarts without opening a connection.
A destructive change still stops and asks, on every pass. --skip-schema-changes-confirmation answers in advance.
Pointing your frontend at it
Every run writes the URL it is listening on to your project-root .env:
TYPEBASE_APP_URL_LOCAL=http://127.0.0.1:8080Nothing reads that key on its own. Typebase has no URL-from-environment resolution, so you write the URL into your own client, and you pick the order. Putting the local key first means a local run wins whenever one is going, with dev and prod behind it:
export const client = createRouterClient<Router>({
url: process.env.TYPEBASE_APP_URL_LOCAL || process.env.TYPEBASE_APP_URL_DEV || process.env.TYPEBASE_APP_URL || '',
});Delete the line from .env, or comment it out, and you are back on dev without touching the client.
Browser code cannot read those directly. It needs whatever prefix your framework uses for public variables, in the same order:
NEXT_PUBLIC_TYPEBASE_APP_URL=$TYPEBASE_APP_URL_LOCALYour framework's guide has the exact form: Next.js, SvelteKit, Nuxt, Expo.
To run two projects at once, give one of them a different port. A port already in use fails immediately and names the flag:
npx typebase-io-cli start --port 8081Auth on localhost
Add your frontend's local origin to trustedOrigins so the local server accepts its requests:
export const auth = defineAuth({
trustedOrigins: ['http://localhost:3000', 'http://localhost:3001'],
emailAndPassword: { enabled: true },
});Use whatever ports your frontend and server actually run on.
The rest is handled for you. A local run generates a BETTER_AUTH_SECRET if your project has none and saves it where a later deploy will find it, so
running locally first does not invalidate every session afterwards. It also writes the local URL into the generated server's auth config as
baseURL, which is what lets better-auth build callbacks and email links.
Environment variables
The server's .env lives in the server cache and is filled from your project-root .env on every pass. You do not
edit it and you do not copy anything across.
Only the keys the server validates are copied: DATABASE_URL, BETTER_AUTH_SECRET, whatever you declared in env.ts, and your
storage credentials when you pass --dev-storage or --prod-storage. Provider tokens
like NEON_API_KEY stay in your project root, where they belong to the CLI rather than the server.
So a variable the server says is missing is missing from your project root too:
✗ Typebase server failed to start.
Invalid environment variables:
RESEND_API_KEY RequiredAdd it to your project-root .env and the next pass picks it up. For the two keys the CLI manages, it can fetch the value from your provider — useful on
a fresh clone:
npx typebase-io-cli env dev get DATABASE_URLnpx typebase-io-cli env dev get BETTER_AUTH_SECRETUse env prod get for the prod branch. If either prints ENCRYPTED, that provider is hiding the raw value, so copy it from the original source: the
Neon console for DATABASE_URL, or your provider's dashboard for BETTER_AUTH_SECRET.
When you want a server you can read
Keeping the build outside your project is what stops a local run from adding files you have to review. The cost is that the generated server is not somewhere you can casually open.
generate-server writes one into typebase/_server/ instead:
npx typebase-io-cli generate-serverReach for it to read the generated code, to hand a build artifact to your own infrastructure, or when you need an adapter or output format a local run would not pick. It generates and stops there, so you install and start it yourself:
cd typebase/_server
npm install
npx tsx src/index.tsIt also never touches a database, so a schema change still needs db <target> push or db <target> migrate alongside it.
Rebuild on every change
--watch rebuilds whenever a file inside typebase/ changes:
npx typebase-io-cli generate-server --watchPress x or Ctrl+C to stop. A build that fails, usually a type error, is printed and leaves the watcher running.
Watch mode rebuilds the files in typebase/_server/, but the server you started from them keeps running the old code until you restart it. Adding
--command restarts it for you.
generate-server --watch --command "pnpm start" gets close to a local run, but you still install by hand, the build still lands in your project tree,
and the database is still yours to keep in step.
Applying a schema change on its own
You do not need a server running to move a schema change into a database:
npx typebase-io-cli db dev pushThis is what deploy dev runs under the hood, so your deployed server and your next deploy stay on the same schema. In migrations mode, use
db dev migrate. For a Postgres on your own machine, point db local push at it:
npx typebase-io-cli db local push --url postgres://localhost:5432/mydbIf a local run is already pointed at that database, it does this for you on every change. See the db reference for both commands.