Typebase.

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 start

Which 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 start

Each 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:

.env
DATABASE_URL_LOCAL=postgres://localhost:5432/mydb

That 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
FlagDatabase
noneDATABASE_URL_LOCAL in your project-root .env
--database-url <url>The connection string you pass
--dev-databaseDATABASE_URL_DEV, your dev branch
--prod-databaseDATABASE_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-storage

That 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. deploy still 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:

.env
TYPEBASE_APP_URL_LOCAL=http://127.0.0.1:8080

Nothing 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:

lib/typebase/client.ts
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:

.env
NEXT_PUBLIC_TYPEBASE_APP_URL=$TYPEBASE_APP_URL_LOCAL

Your 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 8081

Auth on localhost

Add your frontend's local origin to trustedOrigins so the local server accepts its requests:

typebase/auth.ts
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  Required

Add 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_URL
npx typebase-io-cli env dev get BETTER_AUTH_SECRET

Use 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-server

Reach 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.ts

It 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 --watch

Press 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 push

This 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/mydb

If a local run is already pointed at that database, it does this for you on every change. See the db reference for both commands.

On this page