---
name: auth
description: >
  Add user accounts and sign-in to this TanStack Start app. Use when the app
  needs authentication, sign-in, user accounts, protected routes, or per-user
  data. Triggers on "auth", "login", "log in", "sign in", "sign up", "account",
  "users", "authentication", "protected", "who is logged in", "current user",
  "per-user".
metadata:
  short-description: "Auth via the Grok broker (Google, X) or local email/password — no other methods supported"
user-invocable: false
---

# Auth

This app runs its **own** [Better Auth](https://better-auth.com) at
`/api/auth/*`, federating to the shared **Grok auth broker** (`auth.grok.me`)
via the `genericOAuth` plugin. This template wires **Google** and **X**.

**Supported sign-in methods — use ONLY these three; nothing else is supported:
Google, X, and email/password.** No other social/OAuth provider (GitHub, Apple,
Discord, …), no magic links, passkeys, OTP, phone/SMS, or anonymous sign-in. Do
not add entries to `GROK_PROVIDERS`. Method detail and the email/password switch
(edit **only** `src/lib/auth/email-password.ts`): `references/sign-in-methods.md`.
**Exception — connector / app-data apps sign in ONLY via "Continue with Grok"
gate sign-in, no Google/X buttons**: `references/grok-identity.md`.

**Sign-in is OFF by default** — the template ships `.grok/app-env.json` with
`{"VITE_AUTH_ENABLED": "false"}`, so only add accounts when the ask calls for
them (AGENTS.md §0.5). Switching it on is "Turning sign-in on" below.

**Once on, sign-in is REAL — including in the sandbox live preview.** Build real
sign-in; do **NOT** scaffold demo/mock/hardcoded users. Preview: popup + baked
preview client; deployed: per-app client + `DATABASE_URL` + zero-click gate
sign-in (`references/prewired-and-env.md`, `references/grok-identity.md`).

**While OFF** (`VITE_AUTH_ENABLED=false`) a **dev user** is returned so a
non-auth app renders without a signed-in visitor — dev and preview only. The
deployed flag is the deployer's (always `"true"` today), so deployed,
`requireUserId` rejects every visitor; an app without sign-in uses neither
`authMiddleware` nor `requireUserId`.

Everything is **preinstalled and pre-wired in `src/lib/auth/`** — do not
`npm install` anything; `better-auth` is the only auth package (never
`@neondatabase/*`, `@stackframe/*`, or `@clerk/*`). **Do not edit or rewrite any
file under `src/lib/auth/`** — `server.ts` least of all — except
`email-password.ts` for its one flag. Per-file map:
`references/prewired-and-env.md`.

**`/auth/popup` is already handled by the template Vite plugin**
(`vite.config.ts` → `popup.server.ts`): it never paints the React app. **Do NOT
create `src/routes/auth/popup.tsx`** (or any React page / client OAuth at that
path) — that shows the full app inside the popup, the common failure mode.

**Never write a `.env` / `.env.local` / `.env.example`** in this sandbox: live
preview needs **zero** env configuration and a deployed app gets its vars
injected by the platform. The knobs that exist are in
`references/prewired-and-env.md` — never expose a non-`VITE_` var to the client.

`migrations/auth/0001_auth.sql` is the Better Auth schema — **do not edit**. It
sits outside the globbed `migrations/` directory (neither applier descends), so
it is not applied to apps without sign-in; "Turning sign-in on" copies it up.

## Turning sign-in on

Do all of this — the routes alone render the disabled branch:

1. **Flag:** delete the `VITE_AUTH_ENABLED` key from `.grok/app-env.json` and
   **restart the dev server**. Vite reads env at startup, so HMR will not pick
   it up. `npm run dev`, `npm run build` and `npm run preview` all read that
   file through `scripts/with-app-env.mjs`, so preview and the built output flip
   together — never start Vite directly.
2. **Schema:** `cp migrations/auth/0001_auth.sql migrations/0001_auth.sql`, then
   restart so it applies. It is tracked by basename in `_migrations`, so a
   database that already has it will not re-run it.
3. **Routes:** add `src/routes/api/auth/$.ts` + `src/routes/login.tsx` — copy
   both from `references/wiring.md` (the catch-all API route is what makes
   `/api/auth/*` and the broker callback work).
4. **Sign out:** a login with no way out is not done — render `<UserButton />`
   from `@/lib/auth/gates` (it wires `signOut()`); see `references/session-ui.md`.
5. **Existing data:** wrap the app's server functions in `authMiddleware` (an
   auth-off app must not have been using it — see the `neon` skill). Rows from
   before sign-in existed are **development data**: drop and recreate them
   unless the user says otherwise — don't hand them to whoever signs in first.

## Building on it once it's on

- **Sign in / out:** `signIn(providerId)` and `signOut()` from
  `@/lib/auth/client`; `GROK_PROVIDERS` renders the buttons. The popup,
  bearer-token hand-off, and request attachment are internal — leave them alone.
  Prefer `<UserButton />` (it handles the pending and failure states); `signOut()`
  rejects when deployed if the server never confirms — catch it. Never
  `authClient.signOut()`: it leaves the preview bearer token attached to every
  later request, so the visitor stays signed in.
- **Reading the user:** `useCurrentUser()` is display-only (`null` means
  *loading OR signed out*, so never redirect on it alone); guard on
  `useCurrentUserState()`'s `isPending` instead. Gates (`SignedIn`, `SignedOut`,
  `RedirectToSignIn`, `UserButton`) live in `@/lib/auth/gates`. Skeleton/flicker
  rules and cookie-SSR for a zero-flash deployed first paint:
  `references/session-ui.md`.
- **Per-user data (mandatory):** every server function that touches per-user data
  must use the prewired `authMiddleware` and scope every read **and** write to
  `context.userId` — a Postgres driver has full DB access, so nothing else limits
  the query. Keep `user_id` columns `TEXT`; never trust a client-supplied user id;
  signed out, the middleware throws `UnauthorizedError` (401). Code and
  disabled-mode semantics: `references/per-user-data.md`.
- **Security model:** headless broker, `__Host-` cookies + `trustedOrigins`, and
  Fetch-Metadata sibling isolation are already wired — never weaken them to make
  an error go away (`references/sign-in-methods.md` covers the model and the
  "Invalid origin" fix).
