# React Supabase

You are working in a React project backed by Supabase, usually a Next.js 16 App Router app using `@supabase/ssr`, `@supabase/supabase-js` and TypeScript. Write code for the versions in `package.json`: the auth helpers packages, `getSession()` on the server and `middleware.ts` are outdated patterns.

## Before you write code

- Check `package.json` for `@supabase/ssr` and `@supabase/supabase-js`, and whether this is Next.js or a Vite single-page app (see the last section).
- Find the existing client factories (`lib/supabase/client.ts`, `lib/supabase/server.ts`, the proxy helper) and reuse them. There is one factory per environment.
- Read `supabase/migrations/` and the generated types file (`lib/database.types.ts` or similar) before touching data: they are the schema. If `supabase/config.toml` exists, the project runs Supabase locally.
- Environment: `NEXT_PUBLIC_SUPABASE_URL` and the publishable key (`sb_publishable_…`, or the legacy anon key) are public. The secret key (`sb_secret_…`, or the legacy `service_role` key) is server only.

## Clients from @supabase/ssr

```ts
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/lib/database.types";

export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll(cookiesToSet) {
          try {
            for (const { name, value, options } of cookiesToSet) {
              cookieStore.set(name, value, options);
            }
          } catch {
            // Server Components cannot set cookies; proxy.ts refreshes them.
          }
        },
      },
    }
  );
}
```

- Browser: `createBrowserClient<Database>(url, publishableKey)` in `lib/supabase/client.ts`, called from Client Components. It is a singleton in the browser.
- Server: create a new server client per request (as above) in Server Components, Server Functions and Route Handlers. Never share one across requests or store it in a module variable.
- Use only `getAll` and `setAll` for cookies. The old `get`/`set`/`remove` interface is deprecated.
- Pass the generated `Database` type to every client so queries, inserts and RPC calls are typed.

## Sessions in proxy.ts

Next.js 16 uses `proxy.ts` (not `middleware.ts`) to refresh the auth token on each request:

```ts
// proxy.ts
import { type NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";

export async function proxy(request: NextRequest) {
  let response = NextResponse.next({ request });
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll(cookiesToSet) {
          for (const { name, value } of cookiesToSet) request.cookies.set(name, value);
          response = NextResponse.next({ request });
          for (const { name, value, options } of cookiesToSet) {
            response.cookies.set(name, value, options);
          }
        },
      },
    }
  );
  const { data } = await supabase.auth.getClaims(); // refreshes the session
  if (!data?.claims && request.nextUrl.pathname.startsWith("/app")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|webp)$).*)"],
};
```

- Run no code between `createServerClient` and `auth.getClaims()`, and return the same `response` object (or copy its cookies onto a new one). Dropping those cookies logs users out at random.
- On the server, trust `supabase.auth.getClaims()` (verifies the JWT signature) or `supabase.auth.getUser()` (asks the Auth server; use it when a revoked or deleted user must be refused at once). Never authorize with `getSession()` on the server: it reads the cookie without verifying it.
- Proxy redirects are a convenience. Authorize again where the data is read or written; Row Level Security is the final check.
- Auth flows: `signInWithPassword`, `signInWithOtp`, `signInWithOAuth({ provider, options: { redirectTo } })`. OAuth and magic links return to a Route Handler (`app/auth/callback/route.ts`) that calls `exchangeCodeForSession(code)`; email confirmation links call `verifyOtp({ type, token_hash })`. Redirect only to paths you allow.
- After sign in or sign out in a Server Function, `revalidatePath("/", "layout")`, then `redirect()`.

## Row Level Security on every table

- Enable RLS on every table in an exposed schema (`public`) in the same migration that creates it. A table without RLS is readable and writable by anyone holding the publishable key.
- Write one policy per operation and role, with `(select auth.uid())` so Postgres evaluates it once per query, not per row:

```sql
alter table public.notes enable row level security;

create policy "Owners read notes" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "Owners insert notes" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);
```

- `update` needs both `using` and `with check`. Add `to authenticated` (or `anon`) to every policy, and index the columns policies filter on (`user_id`, `org_id`).
- For membership checks across tables, call a `security definer` function in a schema the API does not expose, with `set search_path = ''`, instead of joining inside the policy.
- Never base authorization on `user_metadata` (users can edit it). Use `app_metadata` or your own tables.
- Views bypass RLS unless created `with (security_invoker = true)`.
- Still filter in queries (`.eq("user_id", userId)`): RLS secures, filters make the planner fast.

## Queries and types

- Generate types after every migration: `supabase gen types typescript --local > lib/database.types.ts` (or `--project-id`). Use the generated helpers `Tables<"notes">`, `TablesInsert<"notes">`, `Enums<"status">` instead of hand-written interfaces.
- Every call returns `{ data, error }` and does not throw: check `error` before using `data`. Use `.single()` when a row must exist and `.maybeSingle()` when it may not.
- Select the columns you need (`.select("id, title, author:profiles(name)")`), never `*` into a Client Component.
- Read data in Server Components with the server client and pass plain values down. Under Cache Components, the server client reads cookies: render it inside `<Suspense>` and never call it inside `"use cache"`.
- Put multi-step writes that must succeed together in a Postgres function called with `.rpc()`, not in several client calls.

## Realtime and Storage

- Subscribe in an effect and remove the channel on cleanup, or every remount adds a listener:

```tsx
useEffect(() => {
  const channel = supabase
    .channel(`room:${roomId}`)
    .on("postgres_changes", { event: "INSERT", schema: "public", table: "messages", filter: `room_id=eq.${roomId}` }, onInsert)
    .subscribe();
  return () => {
    supabase.removeChannel(channel);
  };
}, [roomId]);
```

- Postgres Changes needs the table in the `supabase_realtime` publication and respects RLS. For many subscribers or high write rates, use Broadcast; private channels (`{ config: { private: true } }`) are authorized by RLS policies on `realtime.messages`. Use Presence for who is online.
- Storage buckets are private unless they hold truly public assets. Write policies on `storage.objects` scoped by bucket and owner folder (`(storage.foldername(name))[1] = (select auth.uid())::text`), and store files under `<user id>/…`.
- Serve private files with `createSignedUrl(path, seconds)` created on the server after checking access; keep expiry short. Validate type and size before upload.

## Keys, migrations and functions

- The secret or `service_role` key bypasses RLS. Use it only in server code (Route Handlers, Server Functions, Edge Functions, scripts) through a separate admin client, with `auth: { persistSession: false, autoRefreshToken: false }`, and never for a request you could make as the user. Never prefix it `NEXT_PUBLIC_` or import it into a client file.
- Change the schema only through migrations: `supabase migration new <name>`, write SQL, `supabase db reset` locally (replays migrations and `seed.sql`), `supabase db push` to the linked project. Use `supabase db diff` only to draft a migration from changes made in the local Studio, then read it.
- Use Next.js Server Functions for mutations that belong to the app's UI. Use Supabase Edge Functions (Deno, `Deno.serve`) for work that must live beside the database: third-party webhooks, database webhooks, jobs shared by web and mobile clients. Set their secrets with `supabase secrets set`.
- For auth screens, file uploads, realtime chat and avatars, start from the Supabase UI Library (shadcn registry blocks installed with the shadcn CLI) and adapt them, instead of writing them from scratch.

## Vite single-page apps

- Without a server, use `createClient` from `@supabase/supabase-js` with `import.meta.env.VITE_SUPABASE_URL` and the publishable key; the session lives in local storage and RLS is your only authorization.
- Subscribe to `onAuthStateChange` once at the root and call `data.subscription.unsubscribe()` on cleanup. Do not await other Supabase calls inside its callback; defer them.
- Anything that needs the secret key runs in an Edge Function, never in the bundle.

## Do not

- Do not use `@supabase/auth-helpers-nextjs` or `@supabase/auth-helpers-react`, cookie `get`/`set`/`remove` methods, or `middleware.ts` in Next.js 16.
- Do not authorize with `getSession()` on the server, or trust a user ID sent from the client instead of `auth.uid()`.
- Do not create a table without RLS, write `auth.uid()` in a policy without `select`, or put the secret key in client code.
- Do not leave realtime channels subscribed after unmount or make buckets public to avoid writing policies.
- Do not edit the remote schema in the dashboard without a migration.

## Before you finish

- Run the type checker after regenerating types; a type error on a query usually means a stale types file or a wrong column.
- Run `supabase db reset` and test each policy as two different users and as `anon`: one must not see or change the other's rows.
- Check the dashboard's Security and Performance Advisors for tables without RLS, mutable function search paths and unindexed policy columns.
- Confirm no secret key appears in client bundles or `NEXT_PUBLIC_` variables, and list the migrations you added.
