Skip to content

auth-better

Package @theoven/auth-better
Adds to context ctx.user, ctx.auth — it is a provider for auth
Endpoints all of better-auth’s, mounted at /auth/*
Creates files none
Creates tables better-auth’s own — user, session, account, verification, plus whatever your plugins add
Status shipped
Terminal window
bun add @theoven/auth @theoven/auth-better better-auth
src/auth.ts
import { betterAuth } from 'better-auth'
import { Database } from 'bun:sqlite'
export const instance = betterAuth({
database: new Database('./auth.db'),
basePath: '/auth',
secret: env.string('BETTER_AUTH_SECRET'),
emailAndPassword: { enabled: true },
socialProviders: {
github: { clientId: env.string('GITHUB_ID'), clientSecret: env.string('GITHUB_SECRET') },
},
})
src/app.ts
import { auth } from '@theoven/auth'
import { betterAuthProvider } from '@theoven/auth-better'
import { instance } from './auth'
export const app = createApp().use(auth(betterAuthProvider({ instance })))

Run better-auth’s own migration once: bunx @better-auth/cli migrate.

Mounts a better-auth instance you built inside your Oven app, and turns its session into ctx.user. better-auth owns sign-in, sign-up, OAuth callbacks, verification, and its own tables; this brick routes to it and reads sessions back out.

It is the provider that exercises the routes capability at its fullest — the opposite end of the contract from auth-clerk, which mounts nothing at all.

Method Path Purpose Auth
any /auth forwarded to better-auth better-auth’s own
any /auth/* forwarded to better-auth better-auth’s own

GET, POST, PUT, PATCH, DELETE and OPTIONS are all forwarded. A wildcard rather than a list, because better-auth’s endpoint set depends on which plugins are enabled — see Everything, on a wildcard.

Which concrete paths exist is better-auth’s documentation to give, not this page’s: with emailAndPassword you get /auth/sign-up/email and /auth/sign-in/email, with a social provider you also get /auth/sign-in/social and /auth/callback/:provider, and a plugin adds its own.

You configure better-auth; this brick mounts it

Section titled “You configure better-auth; this brick mounts it”

The instance is yours. This brick does not wrap betterAuth(), take a database option, or re-expose its plugins.

better-auth’s configuration is large, moving, and better documented by better-auth. A wrapper around it would be permanently one release behind and would hide whichever option you needed. So the boundary is drawn where it is honest: you build the instance, Oven routes to it and reads sessions from it.

The routes capability at its fullest — the opposite end of the contract from auth-clerk, which mounts nothing at all. The same AuthProvider interface fits both.

Every method under the prefix is forwarded:

GET|POST|PUT|PATCH|DELETE|OPTIONS /auth
GET|POST|PUT|PATCH|DELETE|OPTIONS /auth/*

A wildcard rather than a list, because better-auth’s endpoint set depends on which plugins are enabled — enumerating them would be wrong the moment you added one. The cost is that oven routes shows the wildcard instead of thirty paths, and your OpenAPI document does not describe them. better-auth documents its own endpoints.

ctx.user comes from instance.api.getSession(), called with the request’s headers rather than an extracted token — so whichever scheme you configured, cookie or bearer, keeps working without this brick knowing about it.

ctx.user.raw is better-auth’s user object, including any fields you added with additionalFields.

refresh is not declared: better-auth rotates sessions on its own endpoints, and there is no server-side call for an application to make. Declaring it would offer a method with nothing to do.

The brick itself takes two options. Everything else is configured on the better-auth instance.

auth(betterAuthProvider({ instance, skipBasePathCheck: false }), { prefix: '/auth' })
Option Default
instance — required; the object returned by betterAuth({ ... })
skipBasePathCheck false skips the boot check that basePath matches the mount prefix

The second is deliberately narrow: set it only if you route to better-auth some other way, because what it disables is the check that catches the most common misconfiguration on this page.

prefix belongs to the auth brick and defaults to /auth. Whatever you choose must equal better-auth’s basePath.

Files: none.

Tables: better-auth’s, not Oven’s — user, session, account, verification, plus anything your plugins add. Create them with better-auth’s own CLI:

Terminal window
bunx @better-auth/cli migrate

oven db does not know about this schema and will not generate or apply it.

Guarding a route and reading the user is the same as with any other provider:

src/routes/me.get.ts
export const auth = true
export default async ({ user }) => ({
id: user.id,
email: user.email,
// Anything you added with better-auth's `additionalFields` lives here.
plan: user.raw.plan,
})

Signing out server-side, when better-auth exposes it:

src/routes/logout.post.ts
export default async (ctx) => {
// `ctx.auth` is the provider itself, so its declared capabilities are readable at runtime.
if (!ctx.auth.capabilities?.signOut) throw new BadRequest('This provider cannot sign out.')
await ctx.auth.signOut?.(ctx)
return { ok: true }
}

Clients usually call better-auth’s own endpoints directly instead — POST /auth/sign-in/email, POST /auth/sign-out — which is the path better-auth’s client library takes.

identify ✓ — from instance.api.getSession()
routes ✓ — the whole of better-auth, on a wildcard
signOut ✓ when the instance exposes api.signOut, checked at boot
refresh ✗ — declared false; better-auth rotates sessions on its own endpoints

What fails at boot: an instance that is missing handler or api.getSession is rejected immediately with the line you should have written; and a basePath that disagrees with the mount prefix is refused, naming both values.

Against a real better-auth instance on a real SQLite database — not a double. A double would only prove the double works, and the whole question here is whether better-auth’s own endpoints, whatever they happen to be, survive the trip through Oven’s router.

The suite signs a user up and in through the mounted routes, checks a wrong password is refused, and hits an endpoint the brick never enumerated — the point of the wildcard. Then that the session cookie becomes ctx.user, that a guarded route refuses an anonymous request, that a corrupt cookie leaves public routes working anonymously rather than 500ing, that the prefix itself is routed and not only paths beneath it, and that routes outside the prefix are untouched.

Boot behaviour too: a basePath mismatch fails and names both paths, matching paths boot (trailing slashes included), the check can be waived, a non-instance is refused, and signOut is declared only when the instance offers it.

The schema those tests run against is generated from better-auth’s own table metadata rather than hand-copied, so it cannot drift into a false pass when better-auth adds a column.

  • No OpenAPI for the mounted routes. They arrive as a wildcard, so Oven cannot describe them.
  • Migrations are better-auth’s. Run bunx @better-auth/cli migrate; oven db does not know about its schema.
  • Two session systems if you also use auth-basic. Pick one. Nothing stops you registering both, and nothing good comes of it.
  • The brick tracks better-auth structurally, through the handler and getSession shape rather than its exported types, so it does not pin a version. A breaking change to either would still break it.