---
name: supabase-fullstack
description: "Supabase + Next.js: auth, RLS policies, edge functions, storage, realtime — production patterns. Builds Supabase apps with proper RLS policies, server/client component patterns, edge functions, storage buckets, and realtime subscriptions."
license: MIT
---

# Supabase Fullstack Pro

A skill for building production-grade Supabase backends inside Next.js App Router projects. It focuses on the parts that are easy to get wrong: row-level security, the split between server and client Supabase clients, edge functions, storage access rules, and realtime subscriptions. Aimed at developers who already know Next.js basics but want their Supabase layer to be secure and correct rather than demo-grade.

## When to use it

- You're wiring up `@supabase/ssr` and keep getting `auth.uid()` returning null in your queries.
- You need RLS policies that actually enforce per-user ownership instead of leaving tables wide open.
- You're mixing Server Components, Route Handlers, and Client Components and aren't sure which Supabase client belongs where.
- You want a Deno edge function that validates a webhook or runs privileged logic with the service role key.
- You're adding realtime presence or `postgres_changes` and rows aren't broadcasting because RLS blocks them.

## What it does

The skill scaffolds the canonical `@supabase/ssr` setup: a browser client via `createBrowserClient`, a server client via `createServerClient` wired to Next's `cookies()`, and middleware that refreshes the session token on every request. It writes RLS policies as plain SQL migrations using `auth.uid()`, `using` and `with check` clauses, and reminds you to `alter table ... enable row level security`. For storage it generates bucket policies against `storage.objects`. Edge functions are scaffolded as Deno handlers under `supabase/functions/` for `supabase functions deploy`. Realtime is set up with `supabase.channel().on('postgres_changes', ...)` plus the publication and RLS prerequisites.

## Usage

```bash
curl -fsSL --create-dirs -o ~/.claude/skills/supabase-fullstack/SKILL.md https://claudeskil.com/skills/supabase-fullstack/SKILL.md

# then, in your project:
npm install @supabase/supabase-js @supabase/ssr
supabase init && supabase start
```

```sql
-- migration: per-user row ownership
alter table public.notes enable row level security;

create policy "owner reads" on public.notes
  for select using (auth.uid() = user_id);

create policy "owner writes" on public.notes
  for insert with check (auth.uid() = user_id);
```

## Tips & gotchas

- Never expose the service role key to the browser. It bypasses RLS entirely — keep it server-only in edge functions or Route Handlers.
- A `select` policy needs `using`; an `insert` policy needs `with check`. `update` typically needs both. Mixing them up silently blocks rows.
- Realtime respects RLS. If `postgres_changes` events don't arrive, the subscribing user's RLS policy is filtering them — and the table must be in the `supabase_realtime` publication.
- Always `await supabase.auth.getUser()` (not `getSession()`) on the server; it revalidates the JWT against the auth server.

## FAQ

**Why is `auth.uid()` null in my policy?** The request isn't carrying the user's JWT. With `@supabase/ssr`, ensure middleware refreshes cookies and you use the server client, not the anon browser client.

**Server Component or Client Component for data?** Read in Server Components for speed and security; use the browser client only for realtime and interactive mutations.

**Do I still need RLS if I check auth in my API routes?** Yes. RLS is your last line of defense and protects against any client hitting PostgREST directly.
