Files

2.3 KiB

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). With auth disabled (VITE_AUTH_ENABLED=false) it resolves the dev user ("dev-user") in dev and preview only — the deployed flag comes from the deployer (today always "true"), so deployed it rejects every visitor — which is why an app without sign-in must not use the middleware at all (see the neon skill). 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.