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.