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.