# Supabase Authentication Standard

> **Scope:** nextjs-supabase
> **Layer:** 2 (on keyword)
> **Keywords:** supabase, auth, rls, user, authentication
> **Load When:** supabase auth keywords detected

Stack: Next.js 15 + Supabase + .NET Backend

## Core Rules

- NEVER use `supabase.auth.getSession()` on server -- reads from cookies without validation
- ALWAYS use `supabase.auth.getUser()` on server -- validates JWT with Supabase
- NEVER expose `service_role` key on frontend -- bypasses RLS
- ALWAYS use `@supabase/ssr` for Next.js -- not `@supabase/auth-helpers-nextjs` (deprecated)
- ALWAYS use PKCE flow for SSR auth

## Client Setup

### Browser Client

```ts
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}
```

### Server Client

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

export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll(); },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options));
        },
      },
    }
  );
}
```

## Auth Flows

```ts
// Email/Password sign up
await supabase.auth.signUp({ email, password,
  options: { emailRedirectTo: `${origin}/auth/callback` } });

// Email/Password sign in
await supabase.auth.signInWithPassword({ email, password });

// OAuth (Google / GitHub)
await supabase.auth.signInWithOAuth({
  provider: "google", // or "github"
  options: { redirectTo: `${origin}/auth/callback`,
    queryParams: { access_type: "offline", prompt: "consent" } } // Google-specific
});

// Magic Link
await supabase.auth.signInWithOtp({ email,
  options: { emailRedirectTo: `${origin}/auth/callback` } });
```

## Auth Callback Route (PKCE)

```ts
// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  const next = searchParams.get("next") ?? "/dashboard";
  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) return NextResponse.redirect(`${origin}${next}`);
  }
  return NextResponse.redirect(`${origin}/auth/error`);
}
```

## Middleware Pattern

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

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll(); },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value);
            supabaseResponse.cookies.set(name, value, options);
          });
        },
      },
    }
  );
  const { data: { user } } = await supabase.auth.getUser();
  if (!user && request.nextUrl.pathname.startsWith("/dashboard"))
    return NextResponse.redirect(new URL("/login", request.url));
  return supabaseResponse;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)"],
};
```

## .NET JWT Validation

```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuer = true,
            ValidIssuer = $"https://{supabaseProjectRef}.supabase.co/auth/v1",
            ValidateAudience = true,
            ValidAudience = "authenticated",
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(supabaseJwtSecret)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

// Extract user ID: maps to auth.uid()
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
```

## Environment Variables

| Variable | Where | Purpose |
|----------|-------|---------|
| `NEXT_PUBLIC_SUPABASE_URL` | Frontend | Supabase project URL |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Frontend | Public anon key (respects RLS) |
| `SUPABASE_SERVICE_ROLE_KEY` | Backend ONLY | Bypasses RLS -- NEVER on frontend |
| `SUPABASE_JWT_SECRET` | Backend ONLY | JWT validation secret |

## Common Mistakes

| Wrong | Right | Why |
|-------|-------|-----|
| `getSession()` on server | `getUser()` on server | getSession reads unvalidated cookie data |
| `@supabase/auth-helpers-nextjs` | `@supabase/ssr` | auth-helpers is deprecated |
| `service_role` in `NEXT_PUBLIC_*` | `anon` key in `NEXT_PUBLIC_*` | service_role bypasses all RLS |
| Implicit flow for SSR | PKCE flow with code exchange | Implicit exposes tokens in URL fragments |
| Auth only in page components | Auth check in middleware.ts | Middleware prevents flash of content |
| Missing `setAll` in cookie config | Both `getAll` and `setAll` | Session refresh silently fails without setAll |
