Typebase.

Auth

Add user authentication to your Typebase project.

Last updated on

Typebase uses better-auth for authentication. It supports email/password, social logins (Google, GitHub, etc.), and more, all configured through a simple TypeScript file.

The defineAuth function in Typebase is a thin wrapper around better-auth, so anything you can do with better-auth you can do here. If you need features not covered in these docs (plugins, advanced session config, multi-factor auth, and more), the better-auth documentation is the full reference.

Authentication is optional. If your project doesn't need user accounts, you can skip this entirely. Typebase works just fine without it.

Auth requires a database. better-auth keeps users, sessions, accounts, and verifications in your schema, so a project with an auth.ts must also have a db/schema.ts. When auth.ts exists but db/schema.ts doesn't, codegen, generate-server, deploy, and auth generate all stop with an error instead of building a server whose auth could never work.

Overview

Adding authentication to a Typebase project involves four steps:

  1. Create an auth.ts file in your typebase/ directory to configure authentication.
  2. Run npx typebase-io-cli auth generate to add the required authentication tables to your database schema.
  3. Get the schema into your database with npx typebase-io-cli db dev push (or npx typebase-io-cli deploy dev, which does it as part of deploying). In migrations mode, auth generate has already recorded the migration for you, so apply it with npx typebase-io-cli db dev migrate.
  4. Create an auth client on the frontend to handle sign-up, sign-in, and session management.

The commands below target dev because that's where you'll iterate. Every dev command has a prod twin. Swap in npx typebase-io-cli db prod push or npx typebase-io-cli deploy prod when you're ready to ship. Each target uses its own Neon branch and its own provider-side environment variables, so dev and prod stay isolated.

Step 1: Configure authentication

Create an auth.ts file in the root of your typebase/ directory:

typebase/auth.ts
import { defineAuth } from 'typebase-io/server';

export const auth = defineAuth({
  trustedOrigins: ['http://localhost:3000'],
  emailAndPassword: {
    enabled: true,
  },
});

defineAuth options

The defineAuth function accepts all better-auth configuration options except database (Typebase handles that for you). Here are some common options:

OptionDescription
trustedOriginsURLs allowed to make authentication requests (e.g. your frontend URL)
emailAndPassword.enabledEnable email/password authentication
socialProvidersConfigure social login providers (Google, GitHub, etc.)

Social providers example

typebase/auth.ts
import { defineAuth } from 'typebase-io/server';

export const auth = defineAuth({
  trustedOrigins: ['http://localhost:3000'],
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
});

Plugins

Every built-in better-auth plugin is re-exported through Typebase so you don't have to depend on better-auth directly. Import server plugins from typebase-io/server/auth-plugins and pass them to defineAuth:

typebase/auth.ts
import { defineAuth } from 'typebase-io/server';
import { admin, magicLink } from 'typebase-io/server/auth-plugins';

export const auth = defineAuth({
  trustedOrigins: ['http://localhost:3000'],
  emailAndPassword: { enabled: true },
  plugins: [
    admin(),
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        // send the link via your mailer of choice
      },
    }),
  ],
});

Wire the matching client plugins through typebase-io/client/auth-plugins:

lib/typebase/auth-client.ts
import { createAuthClient } from 'typebase-io/client/auth/react';
import { adminClient, magicLinkClient } from 'typebase-io/client/auth-plugins';

export const authClient = createAuthClient({
  plugins: [adminClient(), magicLinkClient()],
});

Both paths re-export better-auth/plugins and better-auth/client/plugins directly, so every plugin documented in the better-auth plugin reference is available. Plugins shipped in separate packages (for example @better-auth/expo) are still imported from their own package.

Step 2: Generate auth tables

Run the auth generate command to automatically add the required authentication tables (users, sessions, accounts, verifications) to your database schema:

npx typebase-io-cli auth generate

This command does two things:

  1. Appends tables to db/schema.ts: adds the users, sessions, accounts, and verifications tables.
  2. Updates db/relations.ts: registers the new tables and their relationships.
You must have a db/schema.ts and an auth.ts file before running this command.

After running the command, your schema will include something like this:

typebase/db/schema.ts
// ... your existing tables ...

export const users = p.pgTable('users', {
  id: p.text('id').primaryKey(),
  name: p.text('name').notNull(),
  email: p.text('email').notNull().unique(),
  emailVerified: p.boolean('email_verified').default(false).notNull(),
  image: p.text('image'),
  createdAt: p.timestamp('created_at').defaultNow().notNull(),
  updatedAt: p
    .timestamp('updated_at')
    .defaultNow()
    .$onUpdate(() => new Date())
    .notNull(),
});

export const sessions = p.pgTable('sessions', {
  /* ... */
});
export const accounts = p.pgTable('accounts', {
  /* ... */
});
export const verifications = p.pgTable('verifications', {
  /* ... */
});

Step 3: Push the schema to your database

npx typebase-io-cli auth generate only edits source files. It doesn't create the tables in Postgres, so get the schema into your dev database before trying to sign in:

npx typebase-io-cli db dev push

In migrations mode, apply the migration auth generate recorded instead:

npx typebase-io-cli db dev migrate

npx typebase-io-cli deploy dev does the same thing as part of deploying — a push, or your pending migrations — so if you're about to deploy you can skip the explicit step.

Skipping this step is the most common cause of "auth is configured but sign-in fails". The auth client will hit your server, but the server will fail to read/write users, sessions, etc. because the tables don't exist yet.

Step 4: Set up the auth client

Setting up the auth client depends on your frontend framework. Check the integrations section for detailed instructions on sign up, sign in, sign out, server-side sessions, and route protection for your framework.

Protecting actions with auth middleware

Once authentication is set up, you can create protected actions that require a valid session. See the Middleware page for details on creating authenticated actions.

The base URL

better-auth needs to know the URL your server is reachable at, so it can build callback URLs, redirects, and the links it puts in emails.

Typebase fills it in for you, differently depending on where the server is going to answer from:

CommandbaseURL written into the generated auth config
startThe URL the local run listens on, such as http://127.0.0.1:8080
deploy to Vercel{ allowedHosts: ["*.vercel.app"] }
deploy to Cloudflare{ allowedHosts: ["*.workers.dev"] }
deploy to Deno Deploy{ allowedHosts: ["*.deno.dev", "*.deno.net"] }
generate-serverNothing

A deploy names hosts rather than a URL because the deployment URL is not known until the upload finishes; better-auth takes the URL from each incoming request and accepts it only if the host matches. A local run knows its own URL up front — it picked the port — so it writes that URL directly, and --port moves it with you.

Your own baseURL always wins. Typebase only adds one when your defineAuth config does not already have the option, so a value you set is carried through untouched by every command.

generate-server and the base URL warning

generate-server produces an artifact you host yourself, so it cannot know where that will be and writes nothing. better-auth then says so on boot:

WARN [Better Auth]: [better-auth] Base URL could not be determined. Please set a valid
base URL using the baseURL config option or the BETTER_AUTH_URL environment variable.
Without this, callbacks and redirects may not work correctly.

It's a warning, not an error, and the server starts. Sign-in and session reads over your own frontend keep working, because those carry the origin on the request. What breaks is anything better-auth has to build a URL for on its own: social provider callbacks, magic links, and email verification links.

Set BETTER_AUTH_URL in the generated server's .env to wherever it is reachable:

typebase/_server/.env
BETTER_AUTH_URL=http://localhost:8080

Or set baseURL in typebase/auth.ts if the value is the same everywhere you run that server.

Do not hardcode a localhost baseURL in typebase/auth.ts to silence this. Because your own value always wins, it ships on your next deploy and your deployed server builds its callbacks against your laptop. start already sets the local URL for you, so there is nothing to hardcode.

The auth path of an embedded server

Auth is served at /api/auth, better-auth's own default, and the path every generated server uses unless you say otherwise. An embedded server is mounted into an application that already owns its routes, so it can move:

npx typebase-io-cli generate-server --embedded --auth-path /typebase/auth

The value goes to two places at once: the route the generated server registers, and basePath in the generated auth config. That is deliberate, better-auth builds its own URLs from basePath, so if the two disagreed, sign-in would post to one path and be served at another.

--auth-path is embedded-only, like --actions-path. A standalone build rejects the flag and ignores a server.authPath in typebase.json.

When your auth.ts sets a basePath

Your own value always wins, exactly as it does for baseURL. If typebase/auth.ts already sets basePath, Typebase writes nothing over it and registers the route at your path, not the one the flag asked for, so the framework and better-auth still agree. The build warns so you know which one took effect:

⚠ `auth.ts` sets `basePath: "/mine"`, so the generated server serves auth there rather than at `/typebase/auth`.

Drop the option, or drop the basePath from auth.ts, depending on which one you meant.

A basePath your auth.ts computes at runtime — from an environment variable, or through a spread — can't be read at build time, so the build warns that it may override the path you passed. The generated route registration reads auth.options.basePath at runtime instead of a baked-in string, so the route and better-auth still can't disagree. Its fallback is /api/auth, not the --auth-path you passed: an expression like process.env.AUTH_PATH that resolves to undefined serves auth at /api/auth, wherever you meant to put it. Give it a default of its own.

Environment variables

Typebase uses these auth-related environment variables:

VariableDescription
BETTER_AUTH_SECRETA secret key used to sign session tokens. If it's missing, Typebase generates it locally the first time auth setup needs it.
BETTER_AUTH_URLThe URL the server is reachable at. Only needed with generate-server; start and deploy write a baseURL instead. See the base URL

A project with an auth.ts gets BETTER_AUTH_SECRET added to its env schema automatically, so it's validated at boot and typed as env.BETTER_AUTH_SECRET on the action context. You don't declare it in env.ts yourself.

npx typebase-io-cli auth generate may create BETTER_AUTH_SECRET in your project root .env before your first deploy. Later, npx typebase-io-cli deploy syncs that secret to your deployment provider.

You can also manage provider-side variables manually with the npx typebase-io-cli env command.

On this page