Better Auth Starter

Magic-link authentication for Val Town, powered by Better Auth. Click Remix and you have a working auth-protected app — no setup required.

It uses Val Town SQLite for the user/session database and Val Town Email to send the magic links. Sending magic links to anyone other than yourself requires Val Town Pro.

Architecture

Rendering mermaid diagram...

File structure

index.ts                       ← Hono entrypoint; mounts Better Auth at /api/auth/*
backend/
  auth.ts                      ← Better Auth instance (server)
  schema.ts                    ← Creates the user/session/account/verification tables
frontend/
  index.html                   ← HTML shell (loads Twind + React)
  index.tsx                    ← React entrypoint (mounts <App />)
  lib/
    auth-client.ts             ← Better Auth React client (useSession, signIn, signOut)
  components/
    App.tsx                    ← Root: shows <SignIn /> or <Dashboard /> based on session
    SignIn.tsx                 ← Email form that calls signIn.magicLink
    Dashboard.tsx              ← Signed-in view with sign-out button

How auth works

  1. User enters their email and submits the form. The client calls authClient.signIn.magicLink({ email }), which POSTs to /api/auth/sign-in/magic-link.
  2. Better Auth generates a token, stores it in the verification table, and calls our sendMagicLink callback in backend/auth.ts. The callback uses std/email to send the link.
  3. User clicks the link, which opens /api/auth/magic-link/verify?token=…. Better Auth validates the token, creates a row in session, sets a secure cookie, and redirects to /.
  4. Back in React, useSession() re-fetches /api/auth/get-session, the user object becomes available, and <App /> renders <Dashboard />.

Adding more auth methods

Better Auth has a rich plugin ecosystem. Common next steps:

  • Email + password — flip emailAndPassword.enabled to true in backend/auth.ts. No plugin needed.
  • Social login (Google, GitHub, …) — add socialProviders to your auth config and store the client IDs/secrets as env vars. See the social providers docs.
  • Passkeys, 2FA, organizations — add the relevant plugin on the server and the matching client plugin in frontend/lib/auth-client.ts.

After enabling new methods, you may need new database columns. See the database concepts page — for SQLite you'll add the columns directly to backend/schema.ts.

Production checklist

  • Set the BETTER_AUTH_SECRET env var to a random string from generate-random-signing-key.val.run.
  • Narrow baseURL.allowedHosts in backend/auth.ts to the exact hosts you serve from (e.g. ["myapp.com", "*.web.val.run"]).
  • Make sure your Val Town account is on Pro so emails can be sent to addresses other than your own.

Notes

  • The session cookie defaults to 7 days and refreshes itself.
  • Magic-link tokens expire after 5 minutes and are single-use by default.
  • Better Auth uses Kysely under the hood — Val Town's std/sqlite is already an @libsql/client Client, so we hand it straight to LibsqlDialect. Zero database setup.