Zainuldin's profile picture

Zain Ul Din

Supabase Auth

Skill to setup supabase email and oauth authentication in Next.js 16 project

SKILL.md
---

name: supa-auth
description: Implement Supabase email & password and OAuth authentication (sign-up, sign-in, sign-out, password reset, social login) in this Next.js app using @supabase/ssr.
user_invocable: true

---

# Supabase Email & Password + OAuth Authentication

Implement Supabase authentication with email/password and OAuth providers in this Next.js project.

## Before writing any code

1. Read the relevant Next.js guide in `node_modules/next/dist/docs/01-app/` — this is Next.js 16 and may differ from your training data. Heed deprecation notices.
2. Check if `@supabase/supabase-js` and `@supabase/ssr` are already installed by reading `package.json`. Install them with `pnpm add` if missing.
3. Check if a `.env.local` file exists with `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`. If not, create `.env.local.example` with placeholder values and tell the user to fill in their Supabase project credentials.

## Implementation steps

### 1. Supabase client utilities

Create `lib/supabase/`:

- **`client.ts`** — browser client using `createBrowserClient` from `@supabase/ssr`

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

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

- **`server.ts`** — server client using `createServerClient` from `@supabase/ssr`, reading/writing cookies via the Next.js `cookies()` API

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

/**
 * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each
 * function when using it.
 */
export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            );
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    },
  );
}
```

- **`middleware.ts`** — helper that refreshes the session in middleware using `createServerClient`

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

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  });

  // With Fluid compute, don't put this client in a global environment
  // variable. Always create a new one on each request.
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value),
          );
          supabaseResponse = NextResponse.next({
            request,
          });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options),
          );
        },
      },
    },
  );

  // Do not run code between createServerClient and
  // supabase.auth.getClaims(). A simple mistake could make it very hard to debug
  // issues with users being randomly logged out.

  // IMPORTANT: If you remove getClaims() and you use server-side rendering
  // with the Supabase client, your users may be randomly logged out.
  const { data } = await supabase.auth.getClaims();
  const user = data?.claims;

  if (
    !user &&
    !request.nextUrl.pathname.startsWith("/login") &&
    !request.nextUrl.pathname.startsWith("/signup") &&
    !request.nextUrl.pathname.startsWith("/auth")
  ) {
    // no user, potentially respond by redirecting the user to the login page
    const url = request.nextUrl.clone();
    url.pathname = "/auth/login";
    return NextResponse.redirect(url);
  }

  // IMPORTANT: You *must* return the supabaseResponse object as it is.
  // If you're creating a new response object with NextResponse.next() make sure to:
  // 1. Pass the request in it, like so:
  //    const myNewResponse = NextResponse.next({ request })
  // 2. Copy over the cookies, like so:
  //    myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
  // 3. Change the myNewResponse object to fit your needs, but avoid changing
  //    the cookies!
  // 4. Finally:
  //    return myNewResponse
  // If this is not done, you may be causing the browser and server to go out
  // of sync and terminate the user's session prematurely!

  return supabaseResponse;
}
```

### 2. Proxy

Create or update `proxy.ts` at the project root to call the Supabase middleware helper, refreshing the user's session on every request. Configure the matcher to exclude static assets and API routes that don't need auth.

### 3. Auth callback route

Create `app/auth/callback/route.ts` to handle the OAuth/email confirmation callback. Exchange the `code` query param for a session using `supabase.auth.exchangeCodeForSession`.

### 5. Pages

- Place all auth pages under the `(auth)/` route group.
- **`app/(auth)/login/page.tsx`** — login form (email + password) with link to sign-up and forgot password. Calls the browser client.
- **`app/(auth)/signup/page.tsx`** — sign-up form (email + password + confirm password (Optional)). Calls the browser client. Make sure to handle the case if email confirmation is enable or not on supabase dashboard.
- **`app/(auth)/forgot-password/page.tsx`** — form to request a password reset email. Calls the browser client.

Design forms using the library being used in the project (For example, if project is using Shadcn UI use that). Use proper pending/error states.

### 6. OAuth support

Add OAuth (social login) support alongside email/password auth.

- **`lib/supabase/oauth.ts`** — a client-side utility that initiates OAuth sign-in:

```ts
import { createClient } from "@/lib/supabase/client";
import { type Provider } from "@supabase/supabase-js";

export async function signInWithOAuth(provider: Provider, redirectTo?: string) {
  const supabase = createClient();
  const { error } = await supabase.auth.signInWithOAuth({
    provider,
    options: {
      redirectTo: `${window.location.origin}/auth/callback${redirectTo ? `?redirect_to=${encodeURIComponent(redirectTo)}` : ""}`,
    },
  });
  return { error };
}
```

- Add OAuth buttons (e.g. Google, GitHub) to the login and signup pages. The specific providers to include should be **asked to the user** before implementing — do not assume which providers are enabled on their Supabase dashboard.
- OAuth buttons should call `signInWithOAuth` from the browser client. No server action needed.
- The existing `app/auth/callback/route.ts` already handles the OAuth callback via `exchangeCodeForSession` — ensure it also reads the `redirect_to` query param and redirects accordingly.

### 7. Database migration — profiles table

Create an initial SQL migration file at `supabase/migrations/<timestamp>_create_profiles.sql` (use the current UTC timestamp in `YYYYMMDDHHmmss` format).

The migration should:

```sql
-- Create profiles table
create table public.profiles (
  id uuid references auth.users on delete cascade primary key,
  username text not null,
  avatar_url text,
  created_at timestamptz default now() not null,
  updated_at timestamptz default now() not null
);

-- Enable RLS
alter table public.profiles enable row level security;

-- Policies: users can read any profile, but only update their own
create policy "Public profiles are viewable by everyone"
  on public.profiles for select
  using (true);

create policy "Users can update their own profile"
  on public.profiles for update
  using (auth.uid() = id);

-- Auto-create a profile on sign-up via a trigger
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, username, avatar_url)
  values (
    new.id,
    coalesce(new.raw_user_meta_data ->> 'user_name',
             new.raw_user_meta_data ->> 'preferred_username',
             new.raw_user_meta_data ->> 'name',
             split_part(new.email, '@', 1)),
    coalesce(new.raw_user_meta_data ->> 'avatar_url',
             new.raw_user_meta_data ->> 'picture')
  );
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

-- Auto-update updated_at on row change
create or replace function public.handle_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;

create trigger on_profile_updated
  before update on public.profiles
  for each row execute function public.handle_updated_at();
```

Key details:

- **`id`** is a foreign key to `auth.users` with cascade delete.
- **`username`** is required — auto-populated from OAuth metadata (`user_name`, `preferred_username`, or `name`) falling back to the email prefix.
- **`avatar_url`** is optional — auto-populated from OAuth metadata (`avatar_url` or `picture`). Will be `null` for email/password sign-ups.
- **`updated_at`** is automatically maintained via trigger.
- RLS is enabled with sensible default policies.

Tell the user to apply the migration with `supabase db push` or `supabase migration up` depending on their setup.

### 8. Adding utilities

Add following auth utilities:

- Add a `auth-provider.tsx` and `use-auth.ts` hook (by following the project structure/conventions) to fetch the user on client side.
- Add a server side action `get-auth.ts` inside `lib/auth/get-auth.ts` to fetch the user server side.
- Add `redirect_to` query param to login/signup pages that ensure where to redirect. if not specified redirect back to `/`.

## Constraints

- Implement **email + password** and **OAuth** auth. No magic link.
- **Ask the user** which OAuth providers to include before adding them — do not guess.
- Use `@supabase/ssr` — do NOT use the deprecated `@supabase/auth-helpers-nextjs`.
- Use browser client for auth mutations (including OAuth) — no API route handlers except the callback.
- Do not store secrets in client-side code. Only `NEXT_PUBLIC_*` env vars are exposed to the browser.
- OAuth does not require any additional env vars beyond the existing Supabase URL and anon key — provider config lives in the Supabase dashboard.