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,
  auth: {
    emailAndPassword: { enabled: true },
    secret: process.env.AUTH_SECRET,
  },
})
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,
  auth: {
    socialProviders: {
      github: {
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      },
      google: {
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.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. Copy full definitions from examples/standalone/schema.ts.

On this page