Catalog
fairoosansar-coder/auth

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".

v1.0LATEST
NewUpdated Oct 29, 2026

Auth

This app authenticates users by running its own Better Auth at /api/auth/* and federating to the shared Grok auth broker (auth.grok.me) via the genericOAuth plugin. The broker offers the upstream sign-in methods and holds their shared secrets; this app only holds its own per-app client id/secret and names the upstream it wants via each provider's idp hint. This template wires Google and X; the broker handles the actual login, so the app just renders the provider buttons.

Supported sign-in methods — use ONLY these three; nothing else is supported: Google, X, and email/password (see "Supported sign-in methods" below).

Sign-in is ON by default and REAL — including in the sandbox live preview. Build real sign-in; do NOT scaffold demo/mock/hardcoded users.

  • Live preview (*.grok-sandbox.com): the app is an embedded iframe, so sign-in opens a popup (a top-level redirect to the broker can't work inside the iframe) and federates via a baked shared preview client (src/lib/auth/preview.ts). /auth/popup is already handled by the template Vite plugin (vite.config.ts → popup.server.ts) — it 302s straight to the broker/upstream login (never paints the React app) and, on return, posts the session bearer back in a tiny HTML page. Do NOT create src/routes/auth/popup.tsx (a React page there shows the full app in the popup — the common failure mode). Sessions (and email/password users) persist in the app's embedded PGLite DB — the SAME DB as app data — and, since the iframe's cookies are partitioned, ride a bearer token the popup hands back; all of that lives in src/lib/auth. Restarting the preview resets the DB.
  • Deployed: the deployer injects a per-app client + DATABASE_URL, so sign-in persists identities in Postgres.
  • Off: only when VITE_AUTH_ENABLED=false — then a dev user is returned (for building non-auth apps without a signed-in visitor).

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/*.

What's pre-wired (src/lib/auth/)

File Use it for
client.ts Browser client. signIn(providerId), signOut(), authEnabled, GROK_PROVIDERS.
server.ts The Better Auth instance (server-only). Do not edit or rewrite. Import only from /api/auth/$.
email-password.ts Only place to enable local email/password (emailAndPasswordEnabled = true).
popup.server.ts Live-preview popup handler (server-only). Already wired by the Vite plugin — do not create a route for it.
providers.ts GROK_PROVIDERS — the fixed broker upstream list (Google and X only; don't add others).
use-current-user.ts useCurrentUser() / useCurrentUserState() React hooks.
gates.tsx SignedIn, SignedOut, RedirectToSignIn, UserButton.
middleware.ts authMiddleware for server functions → verified context.userId.
verify.server.ts requireUserId() / getSessionUser() (server-only) for manual wiring.

migrations/0001_auth.sql is the Better Auth schema — pre-applied, do not edit.

Env vars — do not create a .env file

Never write a .env / .env.local / .env.example for auth (or anything else) in this sandbox. Live preview sign-in works out of the box with zero env configuration: the server falls back to the baked preview client in src/lib/auth/preview.ts, derives the *.grok-sandbox.com origin per-request, mints a process-stable session secret, and persists sessions in embedded PGLite. Deployed apps get GROK_AUTH_* / BETTER_AUTH_* / DATABASE_URL injected by the platform — still not something you write into a file.

Optional process-env knobs (platform / rare overrides only — do not put these in a file you create):

Var Where Purpose
VITE_AUTH_ENABLED client on by default; set "false" to turn sign-in OFF (dev user). Only client-visible auth flag
BETTER_AUTH_URL server app's own public origin; unset in preview (origin is derived per-request)
BETTER_AUTH_SECRET server signs this app's own sessions (process-stable fallback in preview; survives HMR)
GROK_AUTH_ISSUER server the shared broker (defaults to https://auth.grok.me)
GROK_AUTH_CLIENT_ID / GROK_AUTH_CLIENT_SECRET server per-app client (falls back to the preview client)
DATABASE_URL server when deployed, Better Auth persists here (preview persists to the embedded PGLite — same DB as app data)

Never expose a non-VITE_ var to the client. The preview client id/secret live server-only in src/lib/auth/preview.ts.

Wiring (do this once)

Live-preview popup is PRE-WIRED — do not create it. signIn opens /auth/popup; the template Vite plugin (authPopupPlugin in vite.config.ts) serves it via popup.server.ts. Never add src/routes/auth/popup.tsx (or any React page / client OAuth at that path). Doing so loads the full app shell in the popup ("the app opened instead of Google") — that is always wrong.

1. Mount Better Auth — create the catch-all API route (this is what makes /api/auth/* work; the broker's OAuth callback lands here):

// src/routes/api/auth/$.ts
import { createFileRoute } from "@tanstack/react-router";
import { auth } from "@/lib/auth/server";

export const Route = createFileRoute("/api/auth/$")({
  server: {
    handlers: {
      GET: ({ request }) => auth.handler(request),
      POST: ({ request }) => auth.handler(request),
    },
  },
});

2. Add a sign-in page — buttons that kick off the broker flow. Import from @/lib/auth/client. authEnabled is true by default (preview + deployed), so the buttons show and work in the live preview; the else branch only shows when auth is explicitly disabled (VITE_AUTH_ENABLED=false):

// src/routes/login.tsx
import { createFileRoute } from "@tanstack/react-router";
import { GROK_PROVIDERS, authEnabled, signIn } from "@/lib/auth/client";

export const Route = createFileRoute("/login")({ component: Login });

function Login() {
  return (
    <main className="grid min-h-screen place-items-center p-6">
      <div className="w-full max-w-sm space-y-3">
        <h1 className="text-xl font-semibold">Sign in</h1>
        {authEnabled ? (
          GROK_PROVIDERS.map((p) => (
            <button
              key={p.providerId}
              type="button"
              onClick={() => signIn(p.providerId, { callbackURL: "/" })}
              className="w-full cursor-pointer rounded-md border border-neutral-300 px-4 py-2 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-900"
            >
              Continue with {p.label}
            </button>
          ))
        ) : (
          <p className="text-sm text-neutral-500">Sign-in is disabled.</p>
        )}
      </div>
    </main>
  );
}

RedirectToSignIn sends signed-out users to /login by default (override with <RedirectToSignIn to="/somewhere" />). Style the page however you like — see the design-ui skill.

That's it — call signIn(providerId) from your sign-in buttons. The popup, bearer-token hand-off, and request attachment are all inside src/lib/auth + the Vite plugin; leave them alone.

Reading the user / protecting routes

@/lib/auth/use-current-user (with auth on — the default — these reflect the REAL session, so a preview visitor is signed out until they sign in):

  • useCurrentUser() → AppUser | null — for display. null means loading OR signed out, so never redirect on it alone.
  • useCurrentUserState() → { user, isPending } — for guards: wait for isPending to clear before treating user: null as signed out, or a hard reload bounces signed-in users to sign-in.

State components from @/lib/auth/gates: SignedIn, SignedOut, RedirectToSignIn, UserButton. (When auth is disabled via VITE_AUTH_ENABLED=false they apply dev-user semantics so a non-auth app still renders.)

import { useCurrentUser, useCurrentUserState } from "@/lib/auth/use-current-user";
import { RedirectToSignIn, SignedIn, SignedOut, UserButton } from "@/lib/auth/gates";

function Navbar() {
  const user = useCurrentUser(); // display only — null may just mean "loading"
  return (
    <>
      <span>{user?.displayName ?? "Guest"}</span>
      <SignedOut><a href="/login">Sign in</a></SignedOut>
      <SignedIn><UserButton /></SignedIn>
    </>
  );
}

function AccountPage() {
  const { user, isPending } = useCurrentUserState();
  if (isPending) return null;             // session still resolving
  if (!user) return <RedirectToSignIn />; // client-side Navigate — not window.location
  return <h1>Welcome, {user.displayName}</h1>;
}

Sign out with <UserButton /> or signOut() from @/lib/auth/client.

Session loading is the same in live preview and when deployed: wait for isPending from useCurrentUserState() (backed by /api/auth/get-session). The only live-preview difference is how sign-in starts (popup + bearer hand-off instead of a full-page OAuth redirect) — not how guests vs signed-in users are detected. Prefer <RedirectToSignIn /> (TanStack <Navigate>) over window.location.href = "/login" so a signed-out redirect does not full-reload the SPA.

Preventing auth flicker

useSession() resolves on the client, so a naive UI flashes signed-out → signed-in on load. Rules:

  1. Gate on isPending, not user alone — and render a same-sized skeleton. Showing the SAME placeholder while isPending (server render + first client paint) makes it one clean swap (skeleton → content) with no flash and no SSR hydration mismatch. Don't return null in a slot that then grows — reserve the space:

    import { useCurrentUserState } from "@/lib/auth/use-current-user";
    import { UserButton } from "@/lib/auth/gates";
    
    function AuthSlot() {
      const { user, isPending } = useCurrentUserState();
      if (isPending) return <div className="h-8 w-8 animate-pulse rounded-full bg-black/10" />;
      return user ? <UserButton /> : <a href="/login">Sign in</a>;
    }
    
  2. Guard at a layout boundary (nav / page shell), not in leaf components that mount/unmount — useSession is one shared store, so keep one stable consumer per region instead of re-gating everywhere.

  3. Zero-flash when deployed: SSR the session from the cookie. On a deployed app (and top-level navigations) the session cookie is same-origin, so the server already knows the user on the first request — resolve it in the root route and render the authed shell immediately:

    // src/routes/__root.tsx (excerpt)
    import { createServerFn } from "@tanstack/react-start";
    import { createRootRoute } from "@tanstack/react-router";
    
    const fetchSessionUser = createServerFn({ method: "GET" }).handler(async () => {
      // Cookie path only — works when deployed / on top-level loads.
      const { getSessionUser } = await import("@/lib/auth/verify.server");
      const u = await getSessionUser();
      return u ? { id: u.id, email: u.email } : null;
    });
    
    export const Route = createRootRoute({
      beforeLoad: async () => ({ sessionUser: await fetchSessionUser() }),
      // Merge into the existing root — keep head() / og:image from first scaffold.
      // component: prefer `sessionUser` for the FIRST paint when deployed, then
      // `useCurrentUserState()` for live in-page updates.
    });
    

    Sign-in/out navigate, so beforeLoad re-runs and the context stays fresh; call router.invalidate() if you change auth state without navigating.

    In live preview the session often rides a bearer after popup sign-in, so cookie SSR may still return null until the client useSession() runs with the bearer attached — still gate on isPending, same as when deployed.

The template already enables Better Auth's session.cookieCache, so /get-session answers from a cookie when one is present (no DB round-trip).

Per-user data (server-side — mandatory)

Pair auth with the DB (see the neon skill). A regular Postgres driver has full DB access, so every server function that touches per-user data must verify the caller and scope rows to them. Use the prewired authMiddleware: it resolves the same-origin session to a verified context.userId (and rejects scripted cross-site/sibling requests) — no token threading:

import { createServerFn } from "@tanstack/react-start";
import { getSql } from "@/lib/db";
import { authMiddleware } from "@/lib/auth/middleware";

export const listTodos = createServerFn({ method: "GET" })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    const sql = await getSql();
    // Type the row shape — a server fn's return must be provably serializable.
    return sql<{ id: number; title: string; done: boolean }>`select id, title, done from todos where user_id = ${context.userId} order by id desc`;
  });

// Inputs go through `.validator()` (the current API); the client passes `{ data }`:
export const addTodo = createServerFn({ method: "POST" })
  .validator((title: string) => title.trim())
  .middleware([authMiddleware])
  .handler(async ({ context, data: title }) => {
    if (!title) return;
    const sql = await getSql();
    await sql`insert into todos (user_id, title) values (${context.userId}, ${title})`;
  });
// mutations must scope writes too: `... where id = ${id} and user_id = ${context.userId}`

Call these from client code (effects, event handlers, React Query) — that's where Sec-Fetch-Site: same-origin holds:

useEffect(() => { listTodos().then(setTodos).catch(() => setTodos([])); }, []);

Semantics: signed out → the middleware throws UnauthorizedError (message "Unauthorized", status 401 — match it to send the visitor to sign-in), in the live preview too (real auth). Only when auth is disabled (VITE_AUTH_ENABLED=false) does it resolve the dev user ("dev-user") and never throw. Keep user_id columns TEXT (Better Auth uses text ids; the disabled dev user is 'dev-user'). Never trust a client-supplied user id — only the middleware / requireUserId() result.

Supported sign-in methods

Use only these three — no other method is supported:

  • Google and X — federated through the Grok broker (pre-wired here). The broker federates these two upstreams and nothing else, so do not add entries to GROK_PROVIDERS beyond them (the broker rejects an unknown idp).

  • Email + password — this app's OWN Better Auth, persisted in your database (never the broker, never mocked). Better Auth is DB-backed in BOTH modes — real Postgres when deployed and the embedded PGLite in the sandbox preview — so email/password accounts are stored and survive across requests, in preview too. It's off by default. Enable it by editing only src/lib/auth/email-password.ts:

    // src/lib/auth/email-password.ts
    export const emailAndPasswordEnabled = true; // was false
    

    Do not edit or rewrite src/lib/auth/server.ts (or any other file under src/lib/auth/ except email-password.ts for this flag). That file is pre-wired; "fixing" it by regenerating Better Auth config breaks live-preview sign-in.

    The pre-applied schema already has the account.password column — no migration needed. Then build sign-up / sign-in forms with authClient.signUp.email(...) and authClient.signIn.email(...) from @/lib/auth/client.

    Do not add emailAndPassword as a plugin entry (that is a syntax/type error). Do not invent a new Better Auth config.

    If sign-up/sign-in returns "Invalid origin", do not disable CSRF and do not edit server.ts. The template's trustedOrigins already covers *.grok-sandbox.com and local loopback on port 8080 (localhost / 127.0.0.1 / [::1]). Open the app at one of those origins (not a random host/port).

Do NOT add or use anything else: no other social / OAuth providers (GitHub, Apple, Discord, Microsoft, Facebook, …), and no magic links, passkeys, one-time codes / OTP, phone / SMS, or anonymous sign-in.

Security model (already handled — don't undo it)

  • Headless broker: the app names the upstream (idp); the broker forwards straight to Google/X. Users never see the broker.
  • __Host- cookies + trustedOrigins: a sibling *.grok.me app can't toss a Domain=.grok.me cookie, and Better Auth rejects cross-origin /api/auth calls.
  • Sibling isolation: authMiddleware rejects scripted cross-site/same-site requests (Fetch-Metadata), so a sibling can't ride this app's session cookie into its server functions.
  • The upstream Google/X tokens live only on the broker; this app only ever gets a broker-issued identity and mints its own local session.

Overall Score

84/100

Grade

B

Good

Grades are signals, not a certification. Always review a skill yourself before use.

Safety

87

Quality

82

Clarity

85

Completeness

81

Summary

This skill guides developers to integrate user authentication into a TanStack Start app using Better Auth and a Grok broker for OAuth federation (Google, X) plus optional email/password. The skill is deeply pre-wired: auth code lives in `src/lib/auth/`, the popup handler is baked into the Vite plugin, and sign-in works in live preview with zero configuration. The developer's job is to mount the Better Auth catch-all route, add sign-in/sign-out UI, protect routes with hooks and middleware, and optionally enable email/password by flipping a flag—not by editing the pre-wired config.

Static Analysis Findings

1 finding

Patterns detected by deterministic static analysis before AI scoring. Hover over any finding code for detailed information and remediation guidance.

Credential Exposure
SEC-020Direct .env File Access4x in 1 file

Direct .env file access

SKILL.md.env4x

Detected Capabilities

file readfile write (project-scoped)code generationroute creationserver function creationenvironment variable configuration (read-only guidance)middleware configurationdatabase schema reference

Trigger Keywords

Phrases that agents use to match this skill to user intent.

add authenticationgoogle loginoauth federatedsign-in flowper-user accountsemail password authprotected routessession management

Risk Signals

INFO

SEC-020: Direct .env file access — but EXPLICITLY FORBIDDEN by the skill

SKILL.md | Env vars section
INFO

References to external auth domains (auth.grok.me, better-auth.com) — but these are baked into pre-wired code, not developer-controlled

SKILL.md | intro and Env vars section
INFO

DATABASE_URL environment variable mentioned as platform-injected only — skill forbids developers from creating .env files, reducing credential exposure risk

SKILL.md | Env vars table

Referenced Domains

External domains referenced in skill content, detected by static analysis.

auth.grok.mebetter-auth.com

Use Cases

  • Add Google/X login to a TanStack Start app
  • Integrate email/password authentication without external services
  • Protect routes and server functions behind sign-in
  • Build per-user data access with verified user context
  • Implement sign-in popup in iframe/sandbox environment
  • Enable session persistence across preview restarts and deployments

Quality Notes

  • Excellent scope boundaries: skill explicitly forbids npm install, .env file creation, editing pre-wired auth files, and adding unsupported sign-in methods — this dramatically limits scope creep and misuse
  • Strong error prevention: detailed warnings about common failure modes (e.g., 'never add src/routes/auth/popup.tsx' with explanation of why that breaks), Invalid Origin CSRF trap with troubleshooting
  • Comprehensive pre-wired file reference table (src/lib/auth/*) explains purpose of each file and whether it should be edited — eliminates guesswork
  • Clear separation of concerns: live preview (popup + bearer) vs deployed (cookie + SSR) explained upfront, but session handling is unified, reducing confusion
  • Missing edge cases: what happens if a user signs in, then auth is toggled off mid-session? No guidance on error handling in sign-in/sign-up forms (network failure, duplicate email, etc.)
  • Security model section is defensive and explicit (explains what IS happening and why — headers, cookie restrictions, sibling isolation) — aids understanding but is verbose
  • Code examples are production-ready (class names, real component patterns, proper hook usage) — not scaffolding
  • Minor: 'Do NOT scaffold demo/mock/hardcoded users' is emphatic but would benefit from a note on why (test coverage strategy? production readiness?)
Model: claude-haiku-4-5-20251001Analyzed: Oct 29, 2026

Reviews

Add this skill to your library to leave a review.

No reviews yet

Be the first to share your experience.

Use fairoosansar-coder/auth in your dev environment

Command Palette

Search for a command to run...