5.5 KiB
Data & auth — implementation
Read this after AGENTS.md §0.5 has already said the app needs a database
and/or sign-in. The decision (auth OFF by default, the closed trigger list, "no
migrations / no @/lib/db unless triggered") lives in AGENTS.md, not here.
Full guides + snippets: the neon skill (database) and the auth skill
(sign-in), under .grok/skills/.
Database (@/lib/db, server-only)
const sql = await getSql()from@/lib/db. Use it only insidecreateServerFnhandlers / server loaders.- Dual-mode: a regular Postgres driver (node-postgres,
pg) whenDATABASE_URLis set, else a local PGLite fallback — so the preview always renders. - In preview, PGLite bootstraps at server start (
ensureDbReady) once the app has migrations. Do not remove that. - A deployed app is provisioned a real database when it ships
migrations/*.sqlor sign-in. An app that needs one without either says so with"deploy": {"database": true}in.grok/app-env.json— see theneonskill.
Migrations
migrations/*.sqlis the single schema source: applied to Neon on deploy (npm run buildrunsnpm run db:migrate, so Vercel ships with the schema ready) and to the PGLite preview automatically on startup.- Add the app's tables as ordered files (
migrations/0002_*.sql), not inline. - An app that needs no database adds no
.sqlfile, and then no migration runs anywhere. - The Better Auth schema sits outside that scope in
migrations/auth/(neither applier descends into it); theauthskill's "Turning sign-in on" copies it up. Do not edit it. Applied files are keyed by basename, so a database that already has0001_auth.sqlwill not re-run it.
Server functions
createServerFn with input via .validator() — the current API on the
installed version (.inputValidator() is deprecated). Examples in the neon
and auth skills.
Auth wiring (only once §0.5 says accounts)
- The app runs its own Better Auth at
/api/auth/*and federates to the shared Grok auth broker for Google and X. The only other supported method is this app's own email/password (local Better Auth, off by default — enable only viasrc/lib/auth/email-password.ts; never rewritesrc/lib/auth/server.ts). No other social providers, magic links, passkeys, or OTP/phone. - Two routes:
src/routes/api/auth/$.ts(mounts Better Auth at/api/auth/*) andsrc/routes/login.tsx(provider buttons viasignIn(providerId)). Copy the snippets from theauthskill. - The live-preview popup at
/auth/popupis served by the template Vite plugin (vite.config.ts→popup.server.ts);AGENTS.md§ "First scaffold" states the rule about not adding a React route there. - Read the user with
useCurrentUser()(@/lib/auth/use-current-user) and gate UI withSignedIn/SignedOut/UserButton(@/lib/auth/gates). - Sign-in is real even in the live preview — it federates via a baked shared preview client — so a visitor is signed out until they sign in. Build real sign-in; do NOT scaffold demo/mock/hardcoded users.
- Authorize every server function with
authMiddleware(@/lib/auth/middleware):createServerFn().middleware([authMiddleware])hands the handler a verifiedcontext.userId(resolved from the same-origin session; throws when signed out). Scope every query by thatuser_id. Never trust a client-sent id.
Turning sign-in on (at scaffold or later)
.grok/app-env.json ({"VITE_AUTH_ENABLED": "false"}) is the switch, read by
npm run dev / build / preview alike through scripts/with-app-env.mjs —
which is why Vite is never started outside those scripts. Follow the auth
skill's "Turning sign-in on" (flag, schema, then routes) in that order: the
routes alone render the disabled branch. npm run check:auth, run against a
live dev server, fails when that server and the next build disagree about the
flag (exit 0 agree, 1 diverged, 2 could not observe).
Env
On deploy the platform injects DATABASE_URL + per-app auth creds; live preview
needs neither (baked preview client, PGLite fallback). Deployed behind the gate,
signed-in Grok viewers get the app session automatically from x-grok-identity
(see the auth skill — references/grok-identity.md); the broker federation
covers anonymous viewers and no-gate contexts.
HARD RULE — connector / AppData API (backend only)
Never call connector or AppData APIs from frontend code.
| Allowed | Forbidden |
|---|---|
createServerFn({ method: "POST" }).handler that dynamic-imports @/lib/app-data/client.server and calls callTool |
Importing @/lib/app-data/client.server from a route component, useEffect, event handler, or any client module |
| UI calling that server function only | Browser fetch("/__gate/app-data/…"), fetch to the connectors host, or any direct CallTool from the client |
Types/constants from @/lib/app-data (no network) |
Putting x-connector-access-token, connector JWTs, or gate secrets in client state, props, or VITE_* env |
Flow (required): browser → this app's createServerFn → app backend
SDK → public connectors host (connectors.grok.me, auth required) → gate.
Unauthenticated hits on the connectors host redirect to gate OIDC sign-in.
If you need Drive / Gmail / calendar / connector data, load the app-data
skill (.grok/skills/app-data/SKILL.md) and follow it exactly. Do not invent
a client-side connector client.