Better Auth Example
- Category: Examples
examples/better-auth is the same app as basic — the
IdeaNick ideas board — plus authentication through
Better Auth: sign-in/up, a profile page, protected
pages and mutations, and an error class carrying UNAUTHORIZED / FORBIDDEN
codes. Everything else is unchanged from basic. The buttons above and below open
the full source.
Two things make this the distinctive example. First, Better Auth's own router is mounted as a single middleware on the root — middlewares run server-side only, so the handler body never reaches the browser bundle:
// examples/better-auth/src/lib/root.tsx
.middleware('/api/auth/*', async ({ request }) => await authServer.handler(request.original))That one mount hands every /api/auth/* request to Better Auth's handler, so
its whole endpoint set — sign-in/email, sign-up/email, get-session, and
the rest — is served without you declaring any of them. The full list is owned
by Better Auth, not this repo: see its
API reference.
Second, gating is a plugin. A plugin bundles .ctx (server)
and .with (render, client + SSR), so one .use(...) protects a point
on both sides:
// examples/better-auth/src/lib/auth/plugins.ts
export const authorizedOnlyPlugin = Point0.lets
.plugin('authorizedOnly')
.use(mePlugin) // puts the resolved user into ctx + props
.ctx(({ ctx: { me } }) => {
if (!me)
throw new AppError('Only for authorized users', { code: 'UNAUTHORIZED' })
return { me } // narrowed to non-null
})
.with(({ props: { me } }) => {
if (!me)
return new AppError('Only for authorized users', { code: 'UNAUTHORIZED' }) // return, don't throw
return { me }
})
.plugin()Note the asymmetry: .ctx throws the error (rejecting the server load),
.with returns it (short-circuiting to the error component). The example
ships four such plugins — mePlugin (read the user), authorizedOnlyPlugin and
redirectUnauthorizedPlugin (gate anonymous visitors by error or redirect), and
redirectAuthorizedPlugin (keep signed-in users off the sign-in page). Gating a
point is then one .use(authorizedOnlyPlugin); a protected mutation reads
ctx.me and enforces ownership server-side, while the matching page re-checks
on the client for UX.
The error class adds a code field (here via error0's code
plugin, marked public so it survives serialization), and the error component
branches on it — UNAUTHORIZED renders a sign-in link, FORBIDDEN shows the
message. See .middleware, .with, Plugin,
Env, and Error handling for the mechanics.
Running it
Identical to basic — bun install && bun run build at the repo
root, then bun run setup && bun run dev inside examples/better-auth. The
seed creates a user through Better Auth itself (authServer.api.signUpEmail
with x@example.com / 12345678) and prefills the sign-in form with those
credentials. See getting-started.
For a real app
This example shows auth in isolation. For a real product, start from
start0 — it ships this auth setup with social
providers and more, alongside admin, forms, CRUD, email, and deploy
(bun create start0 my-app).
What this example adds over basic
| Area | basic | better-auth |
|---|---|---|
| Root middleware | OpenAPI only | /api/auth/* Better Auth handler + OpenAPI |
src/lib/auth/ | — | server.ts, client.ts, api.ts, plugins.ts |
| Prisma models | Idea, IdeaNewsPost | adds User, Session, Account, Verification |
| Create / update | one open idea-create-update.tsx | split idea-create.tsx + idea-update.tsx, gated |
| Errors | generic | UNAUTHORIZED / FORBIDDEN codes drive the error UI |
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️