Build Map

Field guide

Real platform surfaces. Real Grok Build / *.grok.me platform. This page is an independent map of it — not the official help center. How to read this →

For builders who ship

Wire it — copy-paste recipes

Actual Grok Build wiring paths: auth mount, per-user SQL, UI gates, xAI server calls, env rules. Paste into your own Grok project — same template skills this platform ships.

Yes — this is the needful path. These snippets match the real pre-wired src/lib/auth + getSql() stack. Do not invent Clerk or rewrite server.ts. Verify broker providers in Live Lab if you extend social login.

Mount auth + login page

When you need: Google / X sign-in on a new Grok Build app

Files: src/routes/api/auth/$.ts · src/routes/login.tsx · src/lib/auth/* (pre-wired — do not rewrite server.ts)

  1. Create the catch-all API route so Better Auth handles /api/auth/*
  2. Add a login page that maps GROK_PROVIDERS → signIn()
  3. Never create src/routes/auth/popup.tsx (preview popup is already in the Vite plugin)
  4. Do not invent GitHub/ChatGPT providers — broker only accepts google + twitter

Providers live in src/lib/auth/providers.ts (Google + X only). Email/password: set emailAndPasswordEnabled = true in src/lib/auth/email-password.ts only.

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),
    },
  },
});
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 rounded-md border px-4 py-2"
            >
              Continue with {p.label}
            </button>
          ))
        ) : (
          <p className="text-sm opacity-60">Sign-in is disabled.</p>
        )}
      </div>
    </main>
  );
}

Per-user data (authMiddleware + SQL)

When you need: Todos / saves / rows that belong to a signed-in user

Files: migrations/0002_*.sql · server functions using createServerFn

  1. Add tables with user_id text not null (TEXT ids, not UUID)
  2. Scope every query with authMiddleware → context.userId
  3. Call server fns from client effects/handlers (same-origin)

Preview uses PGLite (ephemeral). Publish injects DATABASE_URL (Neon). Never trust a client-supplied user id.

migrations/0002_todos.sql
create table if not exists todos (
  id         serial primary key,
  user_id    text not null,
  title      text not null,
  done       boolean not null default false,
  created_at timestamptz not null default now()
);
create index if not exists todos_user_id_idx on todos (user_id);
src/lib/todos.ts
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();
    return sql<{ id: number; title: string; done: boolean }>`select id, title, done from todos where user_id = ${context.userId} order by id desc`;
  });

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})`;
  });

Gate UI on signed-in user

When you need: Show different chrome for guest vs signed-in

Files: any component

  1. Use useCurrentUserState for guards (wait for isPending)
  2. Use SignedIn / SignedOut / UserButton from gates
  3. Prefer RedirectToSignIn over window.location
src/components/nav-auth.tsx
import { useCurrentUserState } from "@/lib/auth/use-current-user";
import { SignedIn, SignedOut, UserButton } from "@/lib/auth/gates";

export function NavAuth() {
  const { user, isPending } = useCurrentUserState();
  if (isPending) {
    return <div className="h-8 w-8 animate-pulse rounded-full bg-black/10" />;
  }
  return (
    <>
      <SignedOut>
        <a href="/login">Sign in</a>
      </SignedOut>
      <SignedIn>
        <span className="text-sm">{user?.displayName}</span>
        <UserButton />
      </SignedIn>
    </>
  );
}

xAI chat (server-only)

When you need: Call Grok from your app without exposing the key

Files: createServerFn only — never client

  1. Read process.env.XAI_API_KEY on the server
  2. User-initiated calls only; cap tokens; spend is the app owner’s
  3. Degrade gracefully if key absent

Docs: https://docs.x.ai — image/video/TTS use the same key, higher cost.

src/lib/ask-grok.ts
import { createServerFn } from "@tanstack/react-start";

export const askGrok = createServerFn({ method: "POST" })
  .validator((input: { prompt: string }) => input)
  .handler(async ({ data }) => {
    const apiKey = process.env.XAI_API_KEY;
    if (!apiKey) return { ok: false as const, error: "AI is not available" };

    const res = await fetch("https://api.x.ai/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model: "grok-4.5",
        messages: [{ role: "user", content: data.prompt }],
        max_tokens: 512,
      }),
    });
    if (!res.ok) {
      return { ok: false as const, error: `xAI API error ${res.status}` };
    }
    const body = (await res.json()) as {
      choices: { message: { content: string } }[];
    };
    return { ok: true as const, text: body.choices[0]?.message.content ?? "" };
  });

Env rules (do not create .env)

When you need: Know what is injected vs forbidden

Files: none — platform injects on publish

  1. Never write .env / .env.local in the Grok Build sandbox for auth/DB
  2. Preview: PGLite + baked preview auth client
  3. Publish: DATABASE_URL, GROK_AUTH_*, BETTER_AUTH_*, optional XAI_API_KEY
  4. Only VITE_* names reach the browser
notes.md
| Var | Where | Purpose |
| --- | --- | --- |
| DATABASE_URL | server | Neon when published |
| GROK_AUTH_ISSUER | server | default https://auth.grok.me |
| GROK_AUTH_CLIENT_ID / SECRET | server | per-app broker client |
| BETTER_AUTH_URL / SECRET | server | app sessions |
| XAI_API_KEY | server | chat / Imagine / voice |
| VITE_AUTH_ENABLED | client | set "false" to disable auth |
| VITE_PUBLIC_HOSTNAME | client | set on publish (e.g. build-map.grok.me) |

Supported social providers only

When you need: Do not break the broker

Files: src/lib/auth/providers.ts — read only unless broker expands

  1. GROK_PROVIDERS is Google + X only
  2. Broker rejects unknown idp (github, chatgpt, apple, …)
  3. Verify live at build-map Live Lab / Bank after platform changes
src/lib/auth/providers.ts (reference)
export const GROK_PROVIDERS = [
  { providerId: "grok-google", idp: "google", label: "Google" },
  { providerId: "grok-x", idp: "twitter", label: "X" },
] as const;