bunderstackbunderstack/ docs

Auth

BetterAuth wired to your Drizzle database

Bunderstack uses BetterAuth under the hood. Auth routes are mounted at /api/auth/*.

Email/password

bunderstack({
  schema,
  database,
  auth: { emailAndPassword: { enabled: true } },
})
curl -X POST /api/auth/sign-up/email \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"pass123","name":"Alice"}'

curl -X POST /api/auth/sign-in/email \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"pass123"}'

OAuth

bunderstack({
  schema,
  database,
  env: envSchema,
  // The builder form receives the validated env, and `db` with it.
  auth: ({ env }) => ({
    socialProviders: {
      github: {
        clientId: env.GITHUB_CLIENT_ID,
        clientSecret: env.GITHUB_CLIENT_SECRET,
      },
      google: {
        clientId: env.GOOGLE_CLIENT_ID,
        clientSecret: env.GOOGLE_CLIENT_SECRET,
      },
    },
  }),
})

Database hooks

BetterAuth hooks often need to write — seed a balance on sign-up, log the event. Pass auth as a builder to get the app's own database instead of opening a second connection:

bunderstack({
  schema,
  auth: ({ db, env }) => ({
    emailAndPassword: { enabled: true },
    databaseHooks: {
      user: {
        create: {
          after: async (user) => {
            await db
              .insert(schema.credits)
              .values({ userId: user.id, amount: 0 })
          },
        },
      },
    },
  }),
})

db is typed from schema alone, so the builder can live in its own file — it never imports the app whose type it helps produce. Same reason api, jobs, and routes take builders. The plain object form keeps working when no hook needs the database.

Reading the session in server functions

// utils/session.ts (TanStack Start)
import { getRequest } from '@tanstack/react-start/server'
import { app } from '~/bunderstack'

export async function getAuthSession() {
  const request = getRequest()
  if (!request) return null
  return app.auth.api.getSession({ headers: request.headers })
}

You can also access app.auth directly — it's just a BetterAuth instance.

Unit testing auth

In unit tests, use the lexical testing fixture with the real email auth lifecycle or header-scoped mocked identities:

import { backend } from './bunderstack'

test('authenticated routes', async () => {
  await using fixture = await backend.test()

  // Real sign-up via Better Auth HTTP handlers
  const identity = await fixture.auth.signUpEmail({
    email: '[email protected]',
    name: 'Alice',
  })
  await fixture.auth.verifyEmail(identity)
  expect(await fixture.auth.getSession(identity)).not.toBeNull()
  const client = fixture.client(identity)

  // Multiple mocked users safely coexist in the same fixture.
  const admin = fixture.auth.mockSession(
    { id: 'admin', email: '[email protected]', name: 'Admin' },
    { activeOrganizationId: 'org_1' },
  )
  const member = fixture.auth.mockSession({
    id: 'member',
    email: '[email protected]',
    name: 'Member',
  })
})

signInEmail() uses the real Better Auth handler and collects its cookies. signOut(identity) invalidates a real session. verifyEmail(identity) follows the latest captured verification link for that user's email.

Required schema tables

Include user, session, account, verification from BetterAuth in your schema. Generate them with npx auth generate, or copy the definitions from the SaaS template.

Better Auth 1.7.6 and later check these tables when the app starts and before every auth call. A missing table or column, or a required column that Better Auth never writes, fails the request with Drizzle schema mismatch. Apps created with Better Auth 1.7.0–1.7.2 may still have a required account.issuer column; follow the 1.7 upgrade guide before removing it.

An app whose schema has no user and session tables runs without Better Auth: every session resolves to null, /api/auth/* answers 404, and the OpenAPI document has no auth paths. Declaring auth without those tables logs a warning at startup.

On this page