| 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 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.
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 — in dev and preview only. The
deployed flag is the deployer's (today it always sets "true"), so deployed,
requireUserId rejects every visitor. An app without sign-in therefore uses
neither authMiddleware nor requireUserId.
Everything is preinstalled and pre-wired in src/lib/auth/ — do not
npm install anything or reach for another auth library. better-auth is the
only auth package; do NOT use @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. The per-file map is in
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 for auth (or anything
else) 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:
- 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.
- 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.
- 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).
- 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.
- 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
written before sign-in existed are development data: drop and recreate
them unless the user says otherwise, rather than handing 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).