<!-- skill: Supabase Build | version: 1.0.0 | description: Build Supabase apps with Postgres RLS, auth, storage and edge functions | category: Web -->
## Instructions
- Postgres first: tables via Dashboard SQL editor or migrations (`supabase db push`); `uuid default gen_random_uuid()` PKs, `created_at timestamptz default now()`, FKs with `on delete cascade` where owned.
- Row Level Security: `alter table posts enable row level security;` then policies per role — `using (auth.uid() = owner_id)` for select/update, `with check` for insert; `service_role` bypasses RLS (server only).
- Policies: public read (`to anon using (published = true)`), owner write (`to authenticated using (auth.uid()=owner_id)`), admin via `auth.jwt() ->> 'role' = 'admin'`; test with two users + anon in SQL editor.
- Auth: email/password + magic link + OAuth (Google/GitHub) in Dashboard Auth settings; client `signUp/signInWithPassword/signOut`, `onAuthStateChange` to hydrate session; SSR reads `sb.auth.getUser()` from cookies.
- JS client: `createClient(url, anonKey)` browser, `createServerClient` in Next RSC/route handlers with cookie adapter; never expose `service_role` key to frontend — edge/server only.
- Storage buckets: `public` bucket for avatars (RLS `storage.objects` policies by `bucket_id`), `private` for docs with `createSignedUrl(path, 3600)`; 5MB client cap, virus-scan sensitive uploads server-side.
- Edge Functions: Deno TS in `supabase/functions/<name>/index.ts`, `supabase functions serve/deploy`; secrets via `supabase secrets set KEY=...`; CORS headers + `verify_jwt` flag per function.
- Realtime: enable per table (Publication), client `.channel('room').on('postgres_changes', { event:'INSERT', table:'messages' })`; RLS applies — user only gets rows they can select.
- Querying: `select('id,title,author:profiles(name)')` embeds, `.eq/.ilike/.order/.range(0,9)` filters, `.single()` for one row; RPC for complex joins (`rpc('nearby_shops', { lat, lng })`).
- Migrations/seed: `supabase init && supabase db diff -f add_posts` generates SQL, review then `db push`; `seed.sql` for demo rows; separate `dev/staging/prod` projects with distinct keys.
- Quotas/costs: free tier pauses idle projects — cron ping or upgrade; monitor Auth MAU, storage GB, edge invocations; enable Point-in-Time Recovery before real data.
- Errors: PostgREST codes (`PGRST116` no rows), RLS denials surface as empty sets (not 403) — debug by `select auth.uid()` + policy `EXPLAIN`; log function errors with request id.
## Examples
- Blog MVP: `posts` table + owner RLS, anon reads published, auth write own, storage cover images, edge `resize-image` function, Next.js SSR list.
- Team chat: `messages` + `rooms` + membership RLS, realtime channel per room, presence typing, signed-URL attachments in private bucket.
- SaaS billing hook: edge function `stripe-webhook` verifies sig, upserts `subscriptions` via service_role, RLS keeps users to own rows, cron dunning via pg_cron.
## Workflows
1. Create project, note URL + anon/service keys, init local `supabase init`.
2. Design tables + FKs + indexes, enable RLS, write select/insert/update policies.
3. Wire auth (providers, redirects), JS client (browser + server variants).
4. Add storage buckets + object policies + signed-URL flows.
5. Build edge functions for secrets/webhooks/transforms, set secrets, deploy.
6. Test as anon/user-A/user-B, push migration, promote dev→prod keys.
## Templates
- Tables + RLS (SQL):
```sql
create table profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  name text not null, avatar_url text,
  created_at timestamptz default now()
);
create table posts (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references profiles(id) on delete cascade not null,
  title text not null, body text not null, published boolean default false,
  created_at timestamptz default now()
);
alter table posts enable row level security;
create policy "public read published" on posts
  for select to anon, authenticated using (published = true);
create policy "owners full access" on posts
  for all to authenticated
  using (auth.uid() = owner_id) with check (auth.uid() = owner_id);
```
- JS client (browser + server):
```js
// browser
import { createClient } from '@supabase/supabase-js';
export const sb = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
const { data } = await sb.from('posts')
  .select('id,title,author:profiles(name)')
  .eq('published', true).order('created_at', { ascending: false }).range(0, 9);
// Next.js server component
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
const store = cookies();
const srv = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  { cookies: { get: (k) => store.get(k)?.value } });
const { data: { user } } = await srv.auth.getUser();
```
- Edge function + storage upload:
```ts
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
serve(async (req) => {
  if (req.method === 'OPTIONS')
    return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*' } });
  const { name } = await req.json();
  return Response.json({ hello: name ?? 'world' },
    { headers: { 'Access-Control-Allow-Origin': '*' } });
});
```
```js
// upload avatar (public bucket + upsert)
const path = `${user.id}/avatar.png`;
await sb.storage.from('avatars').upload(path, file, { upsert: true, contentType: file.type });
const { data } = sb.storage.from('avatars').getPublicUrl(path);
```
## Tools
- SARAH shell_run for `supabase init/start/status/db push/functions serve+deploy/secrets set`, `psql` checks.
- SARAH file_write for migrations, RLS SQL, edge functions, client scaffolds; Supabase Dashboard + Table editor for visual work.
## Advanced
- pg extras: `pg_trgm` fuzzy search (`where title % 'hello'`), PostGIS `nearby` RPC, `pg_cron` nightly cleanup, `pg_net` HTTP calls from triggers; `EXPLAIN ANALYZE` every new RLS policy.
- Auth hardening: custom SMTP, `confirm email` on, leaked-password protection, MFA TOTP (`mfa.enroll`), `auth.admin.*` only via service_role edge functions with audit log.
- Prod topology: separate projects per env, `db dump/restore` promotion, PITR on, `supabase inspect db` bloat/vacuum, read replica when `pg_stat_statements` shows hot reads.
## Documentation
- https://supabase.com/docs
- https://supabase.com/docs/guides/auth
- https://supabase.com/docs/guides/database/row-level-security
- https://supabase.com/docs/guides/storage
- https://supabase.com/docs/guides/functions
