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:
- Create an
auth.tsfile in yourtypebase/directory to configure authentication. - Run
npx typebase-io-cli auth generateto add the required authentication tables to your database schema. - Get the schema into your database with
npx typebase-io-cli db dev push(ornpx typebase-io-cli deploy dev, which does it as part of deploying). In migrations mode,auth generatehas already recorded the migration for you, so apply it withnpx typebase-io-cli db dev migrate. - 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:
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:
| Option | Description |
|---|---|
trustedOrigins | URLs allowed to make authentication requests (e.g. your frontend URL) |
emailAndPassword.enabled | Enable email/password authentication |
socialProviders | Configure social login providers (Google, GitHub, etc.) |
Social providers example
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:
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:
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 generateThis command does two things:
- Appends tables to
db/schema.ts: adds theusers,sessions,accounts, andverificationstables. - Updates
db/relations.ts: registers the new tables and their relationships.
db/schema.ts and an auth.ts file before running this command.After running the command, your schema will include something like this:
// ... 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 pushIn migrations mode, apply the migration auth generate recorded instead:
npx typebase-io-cli db dev migratenpx 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:
| Command | baseURL written into the generated auth config |
|---|---|
start | The 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-server | Nothing |
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:
BETTER_AUTH_URL=http://localhost:8080Or 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/authThe 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:
| Variable | Description |
|---|---|
BETTER_AUTH_SECRET | A secret key used to sign session tokens. If it's missing, Typebase generates it locally the first time auth setup needs it. |
BETTER_AUTH_URL | The 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.