erikpr1994 avatar

supabase-patterns

Supabase client setup, Row Level Security (RLS), edge functions, and real-time. Use when working wit

提供方 erikpr1994|开源

Supabase Patterns

Overview

Decision guide for Supabase patterns focusing on client setup, RLS policies, and edge functions.

Client Setup

Server vs Browser Client

// lib/supabase/server.ts - Server Component / Server Action
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export function createClient() {
  const cookieStore = cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookies) => cookies.forEach(c => cookieStore.set(c)),
      },
    }
  );
}

// lib/supabase/client.ts - Client Component
import { createBrowserClient } from '@supabase/ssr';

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

Decision Matrix

ContextClient TypeUse Case
Server ComponentServerData fetching
Server ActionServerMutations
Route HandlerServerAPI routes
Client ComponentBrowserReal-time, user interactions
MiddlewareServerAuth checks

Row Level Security (RLS)

Essential Policies

-- Enable RLS (REQUIRED)
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

-- Users can only see their own data
CREATE POLICY "Users read own data"
ON todos FOR SELECT
USING (auth.uid() = user_id);

-- Users can only insert their own data
CREATE POLICY "Users insert own data"
ON todos FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can only update their own data
CREATE POLICY "Users update own data"
ON todos FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- Users can only delete their own data
CREATE POLICY "Users delete own data"
ON todos FOR DELETE
USING (auth.uid() = user_id);

Multi-Tenant RLS

-- Organization-based access
CREATE POLICY "Org members access"
ON documents FOR ALL
USING (
  org_id IN (
    SELECT org_id FROM org_members
    WHERE user_id = auth.uid()
  )
);

-- Role-based within org
CREATE POLICY "Admins can delete"
ON documents FOR DELETE
USING (
  EXISTS (
    SELECT 1 FROM org_members
    WHERE user_id = auth.uid()
    AND org_id = documents.org_id
    AND role = 'admin'
  )
);

Query Patterns

// Basic CRUD
const { data, error } = await supabase
  .from('todos')
  .select('*')
  .eq('user_id', userId)
  .order('created_at', { ascending: false });

// Joins (foreign key relationship)
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:users(name, avatar),
    comments(id, content)
  `);

// Upsert
await supabase
  .from('profiles')
  .upsert({ id: userId, name }, { onConflict: 'id' });

// Transaction-like (use RPC)
await supabase.rpc('transfer_funds', { from_id, to_id, amount });

Edge Functions

// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  // Get auth header
  const authHeader = req.headers.get('Authorization')!;

  // Create client with user's JWT
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  );

  // RLS applies automatically
  const { data } = await supabase.from('todos').select('*');

  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' },
  });
});

Anti-Patterns

Anti-PatternProblemSolution
RLS disabledSecurity vulnerabilityAlways enable RLS
Service role in browserBypasses RLSUse anon key client-side
SELECT * without filtersPerformanceSelect specific columns
No error handlingSilent failuresCheck error always
Auth in RLS without indexSlow queriesIndex user_id columns
// BAD: Ignoring errors
const { data } = await supabase.from('todos').select('*');

// GOOD: Handle errors
const { data, error } = await supabase.from('todos').select('*');
if (error) throw new Error(error.message);

Real-time Patterns

// Subscribe to changes
const channel = supabase
  .channel('todos')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'todos' },
    (payload) => console.log(payload)
  )
  .subscribe();

// Cleanup
useEffect(() => {
  return () => { supabase.removeChannel(channel); };
}, []);

Red Flags

  • RLS not enabled on tables with user data
  • Service role key exposed in client code
  • Missing auth.uid() checks in policies
  • No indexes on columns used in RLS policies
  • Real-time subscriptions without cleanup

Quick Reference

-- Check if RLS enabled
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';

-- View policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';

-- Helper function for org membership
CREATE FUNCTION is_org_member(org_id uuid)
RETURNS boolean AS $$
  SELECT EXISTS (
    SELECT 1 FROM org_members WHERE user_id = auth.uid() AND org_id = $1
  );
$$ LANGUAGE sql SECURITY DEFINER;