supabase

Installation
SKILL.md

Supabase Skill

Backend-as-a-Service with PostgreSQL, authentication, real-time subscriptions, storage, and edge functions.

Triggers

Use this skill when:

  • Setting up Supabase client in React/Next.js projects
  • Implementing authentication (email, OAuth, magic links)
  • Writing Row Level Security (RLS) policies
  • Building real-time subscriptions and live updates
  • Managing file uploads with Supabase Storage
  • Creating Supabase Edge Functions (Deno)
  • Generating TypeScript types from database schema
  • Working with Supabase CLI for local development
  • Keywords: supabase, baas, authentication, rls, realtime, edge functions, storage, postgresql

Quick Reference

Feature Use Case
Auth User authentication with email, OAuth, magic links
Database PostgreSQL with Row Level Security (RLS)
Realtime Live subscriptions to database changes
Storage File uploads with access control
Edge Functions Serverless TypeScript functions (Deno)

Client Setup

Installation

npm install @supabase/supabase-js

Basic Client (TypeScript)

// lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
import type { Database } from "./database.types";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);

Next.js App Router Setup

// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/types/database.types";

export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  );
}
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/types/database.types";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options),
            );
          } catch {
            // Called from Server Component - ignore
          }
        },
      },
    },
  );
}

React Context Provider

// providers/supabase-provider.tsx
'use client';

import { createContext, useContext, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@/types/database.types';

type SupabaseContext = {
  supabase: SupabaseClient<Database>;
};

const Context = createContext<SupabaseContext | undefined>(undefined);

export function SupabaseProvider({ children }: { children: React.ReactNode }) {
  const [supabase] = useState(() => createClient());

  return (
    <Context.Provider value={{ supabase }}>
      {children}
    </Context.Provider>
  );
}

export function useSupabase() {
  const context = useContext(Context);
  if (!context) {
    throw new Error('useSupabase must be used within SupabaseProvider');
  }
  return context;
}

Authentication

Email/Password Authentication

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: "user@example.com",
  password: "secure-password",
  options: {
    data: {
      full_name: "John Doe",
      avatar_url: "https://example.com/avatar.png",
    },
  },
});

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: "user@example.com",
  password: "secure-password",
});

// Sign out
await supabase.auth.signOut();

OAuth Providers

// Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: "google",
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
    queryParams: {
      access_type: "offline",
      prompt: "consent",
    },
  },
});

// GitHub OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: "github",
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
    scopes: "read:user user:email",
  },
});

Magic Link (Passwordless)

// Send magic link
const { data, error } = await supabase.auth.signInWithOtp({
  email: "user@example.com",
  options: {
    emailRedirectTo: `${window.location.origin}/auth/callback`,
  },
});

Auth State Management

// Get current session
const {
  data: { session },
} = await supabase.auth.getSession();

// Get current user
const {
  data: { user },
} = await supabase.auth.getUser();

// Listen to auth state changes
const {
  data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
  console.log("Auth event:", event);
  console.log("Session:", session);

  if (event === "SIGNED_IN") {
    // Handle sign in
  } else if (event === "SIGNED_OUT") {
    // Handle sign out
  }
});

// Cleanup
subscription.unsubscribe();

Auth Hook (React)

// hooks/useAuth.ts
import { useEffect, useState } from "react";
import { useSupabase } from "@/providers/supabase-provider";
import type { User, Session } from "@supabase/supabase-js";

export function useAuth() {
  const { supabase } = useSupabase();
  const [user, setUser] = useState<User | null>(null);
  const [session, setSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });

    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session);
      setUser(session?.user ?? null);
    });

    return () => subscription.unsubscribe();
  }, [supabase]);

  return { user, session, loading };
}

Database Operations

CRUD Operations

// SELECT - Fetch all
const { data, error } = await supabase.from("posts").select("*");

// SELECT - With relations
const { data, error } = await supabase.from("posts").select(`
    id,
    title,
    content,
    created_at,
    author:users(id, name, avatar_url),
    comments(id, content, created_at)
  `);

// SELECT - With filters
const { data, error } = await supabase
  .from("posts")
  .select("*")
  .eq("status", "published")
  .gte("created_at", "2024-01-01")
  .order("created_at", { ascending: false })
  .limit(10);

// INSERT
const { data, error } = await supabase
  .from("posts")
  .insert({
    title: "New Post",
    content: "Post content here",
    user_id: user.id,
  })
  .select()
  .single();

// UPDATE
const { data, error } = await supabase
  .from("posts")
  .update({ title: "Updated Title" })
  .eq("id", postId)
  .select()
  .single();

// UPSERT
const { data, error } = await supabase
  .from("user_settings")
  .upsert({
    user_id: user.id,
    theme: "dark",
    notifications: true,
  })
  .select()
  .single();

// DELETE
const { error } = await supabase.from("posts").delete().eq("id", postId);

Advanced Queries

// Full-text search
const { data, error } = await supabase
  .from("posts")
  .select("*")
  .textSearch("title", "typescript react", {
    type: "websearch",
    config: "english",
  });

// Count
const { count, error } = await supabase
  .from("posts")
  .select("*", { count: "exact", head: true })
  .eq("user_id", userId);

// Pagination
const { data, error } = await supabase.from("posts").select("*").range(0, 9); // First 10 items

// OR conditions
const { data, error } = await supabase
  .from("posts")
  .select("*")
  .or("status.eq.published,status.eq.draft");

Row Level Security (RLS)

Enable RLS

-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Force RLS for table owner too
ALTER TABLE posts FORCE ROW LEVEL SECURITY;

Common RLS Policies

-- Users can read all published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');

-- Users can only read their own drafts
CREATE POLICY "Users can view own drafts"
ON posts FOR SELECT
USING (auth.uid() = user_id AND status = 'draft');

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

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

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

Role-Based Access

-- Create profiles table with role
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id),
  role TEXT DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator'))
);

-- Helper function to get user role
CREATE OR REPLACE FUNCTION get_user_role()
RETURNS TEXT AS $$
  SELECT role FROM profiles WHERE id = auth.uid();
$$ LANGUAGE SQL SECURITY DEFINER;

-- Admins can do everything
CREATE POLICY "Admins have full access"
ON posts FOR ALL
USING (get_user_role() = 'admin');

-- Moderators can update any post
CREATE POLICY "Moderators can update posts"
ON posts FOR UPDATE
USING (get_user_role() = 'moderator');

Real-time Subscriptions

Subscribe to Changes

// Subscribe to all changes on a table
const channel = supabase
  .channel("posts-changes")
  .on(
    "postgres_changes",
    { event: "*", schema: "public", table: "posts" },
    (payload) => {
      console.log("Change received:", payload);
    },
  )
  .subscribe();

// Subscribe to INSERT only
const channel = supabase
  .channel("new-posts")
  .on(
    "postgres_changes",
    { event: "INSERT", schema: "public", table: "posts" },
    (payload) => {
      console.log("New post:", payload.new);
    },
  )
  .subscribe();

// Subscribe with filter
const channel = supabase
  .channel("user-posts")
  .on(
    "postgres_changes",
    {
      event: "*",
      schema: "public",
      table: "posts",
      filter: `user_id=eq.${userId}`,
    },
    (payload) => {
      console.log("User post changed:", payload);
    },
  )
  .subscribe();

// Cleanup
supabase.removeChannel(channel);

React Hook for Real-time

// hooks/useRealtimePosts.ts
import { useEffect, useState } from "react";
import { useSupabase } from "@/providers/supabase-provider";
import type { Post } from "@/types";

export function useRealtimePosts(userId?: string) {
  const { supabase } = useSupabase();
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    // Initial fetch
    const fetchPosts = async () => {
      const query = supabase.from("posts").select("*");
      if (userId) query.eq("user_id", userId);
      const { data } = await query;
      if (data) setPosts(data);
    };

    fetchPosts();

    // Subscribe to changes
    const channel = supabase
      .channel("realtime-posts")
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "posts",
          filter: userId ? `user_id=eq.${userId}` : undefined,
        },
        (payload) => {
          if (payload.eventType === "INSERT") {
            setPosts((prev) => [payload.new as Post, ...prev]);
          } else if (payload.eventType === "UPDATE") {
            setPosts((prev) =>
              prev.map((p) =>
                p.id === payload.new.id ? (payload.new as Post) : p,
              ),
            );
          } else if (payload.eventType === "DELETE") {
            setPosts((prev) => prev.filter((p) => p.id !== payload.old.id));
          }
        },
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [supabase, userId]);

  return posts;
}

Storage

Upload Files

// Upload file
const { data, error } = await supabase.storage
  .from("avatars")
  .upload(`${userId}/avatar.png`, file, {
    cacheControl: "3600",
    upsert: true,
    contentType: "image/png",
  });

// Upload from form input
async function uploadFile(event: React.ChangeEvent<HTMLInputElement>) {
  const file = event.target.files?.[0];
  if (!file) return;

  const fileExt = file.name.split(".").pop();
  const fileName = `${Math.random()}.${fileExt}`;
  const filePath = `${userId}/${fileName}`;

  const { data, error } = await supabase.storage
    .from("uploads")
    .upload(filePath, file);

  if (error) throw error;
  return data.path;
}

Get Public URL

// Get public URL (for public buckets)
const { data } = supabase.storage
  .from("avatars")
  .getPublicUrl("user123/avatar.png");

console.log(data.publicUrl);

// Create signed URL (for private buckets)
const { data, error } = await supabase.storage
  .from("private-files")
  .createSignedUrl("document.pdf", 3600); // 1 hour expiry

console.log(data?.signedUrl);

Edge Functions

Create Edge Function

# Initialize new function
supabase functions new my-function

# Deploy function
supabase functions deploy my-function

Edge Function Template

// 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";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
};

serve(async (req) => {
  // Handle CORS preflight
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

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

    // Get authenticated user
    const {
      data: { user },
      error: authError,
    } = await supabase.auth.getUser();
    if (authError || !user) {
      return new Response(JSON.stringify({ error: "Unauthorized" }), {
        status: 401,
        headers: { ...corsHeaders, "Content-Type": "application/json" },
      });
    }

    // Parse request body
    const { name } = await req.json();

    // Your logic here
    const result = { message: `Hello, ${name}!`, userId: user.id };

    return new Response(JSON.stringify(result), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
});

Invoke Edge Function

const { data, error } = await supabase.functions.invoke("my-function", {
  body: { name: "World" },
});

Type Generation

Generate Types from Schema

# Login to Supabase CLI
supabase login

# Generate types
supabase gen types typescript --project-id your-project-id > types/database.types.ts

# Or from local database
supabase gen types typescript --local > types/database.types.ts

Local Development

Supabase CLI Setup

# Install CLI
npm install supabase --save-dev

# Initialize project
npx supabase init

# Start local Supabase
npx supabase start

# Stop local Supabase
npx supabase stop

Database Migrations

# Create migration
npx supabase migration new create_posts_table

# Apply migrations locally
npx supabase db reset

# Push to remote
npx supabase db push

Best Practices

Security

Practice Implementation
Always enable RLS ALTER TABLE x ENABLE ROW LEVEL SECURITY
Use service role only server-side Never expose service_role key
Validate on server Edge Functions for sensitive operations
Secure file uploads Storage policies with user folder patterns

Performance

Practice Implementation
Select only needed columns .select('id, title') not .select('*')
Use indexes CREATE INDEX idx_posts_user ON posts(user_id)
Paginate large datasets .range(0, 9) or cursor pagination
Cache with SWR/React Query Reduce database calls

Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

# Server-side only (never expose)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Common Patterns

Middleware Auth Check (Next.js)

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

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value),
          );
          response = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options),
          );
        },
      },
    },
  );

  const {
    data: { user },
  } = await supabase.auth.getUser();

  // Protect routes
  if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return response;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Error Handling Wrapper

async function safeQuery<T>(
  queryFn: () => Promise<{ data: T | null; error: Error | null }>,
): Promise<T> {
  const { data, error } = await queryFn();

  if (error) {
    console.error("Supabase error:", error);
    throw new Error(error.message);
  }

  if (!data) {
    throw new Error("No data returned");
  }

  return data;
}

// Usage
const post = await safeQuery(() =>
  supabase.from("posts").select("*").eq("id", id).single(),
);
Related skills

More from housegarofalo/claude-code-base

Installs
4
GitHub Stars
2
First Seen
Mar 15, 2026