Supabase Cursor Rules: Open-Source Firebase Alternative

Cursor rules for Supabase covering Postgres database, Auth, Row Level Security, Realtime subscriptions, Edge Functions, Storage, supabase-js client, and local development with the CLI.

June 10, 2026by PromptGenius Team
supabasecursor-rulespostgresbackendauthrealtimedatabase
Supabase Cursor Rules: Open-Source Firebase Alternative

Overview

Supabase is an open-source Firebase alternative built on Postgres, providing database, authentication, real-time subscriptions, file storage, and edge functions. These cursor rules enforce Row Level Security policies, supabase-js client patterns, type-safe queries with generated types, auth flows, and migration workflows so AI assistants generate secure, production-ready Supabase code.

Note:

Enforces Row Level Security on all tables, supabase-js typed client with Database generics, auth patterns (email/password, OAuth, magic links), Realtime subscriptions with channel management, Storage bucket policies, and local development with supabase CLI.

Rules Configuration

---
description: Enforces Supabase best practices including Row Level Security, typed supabase-js queries, Auth patterns, Realtime subscriptions, Storage management, Edge Functions, and CLI-based migration workflows.
globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx,sql/**/*.sql,supabase/**/*
---
# Supabase Best Practices

You are an expert in Supabase, PostgreSQL, and building secure full-stack applications.
You understand Row Level Security, real-time subscriptions, authentication flows, and database schema design.

### Client Setup
- Import createClient from @supabase/supabase-js
- Use the anon key for client-side code, service_role key only for server-side admin operations
- Generate types with `npx supabase gen types typescript --linked > src/types/supabase.ts`
- Use the Database type generic: `const supabase = createClient<Database>(url, anonKey)`
- Never expose service_role key in client bundles or environment variables prefixed with NEXT_PUBLIC_
- Store Supabase URL and anon key in environment variables

### Row Level Security (RLS)
- Enable RLS on ALL tables by default with `ALTER TABLE table_name ENABLE ROW LEVEL SECURITY`
- Create policies for each operation: SELECT, INSERT, UPDATE, DELETE
- Use `auth.uid()` to restrict access to the authenticated user's own data
- Example: `CREATE POLICY "Users own data" ON profiles FOR ALL USING (auth.uid() = user_id)`
- Create separate policies for public read access when needed
- Test policies with `supabase db test` or manual role switching
- Never disable RLS on tables containing user data
- Use security definer functions for complex access patterns that require elevated privileges

### Authentication
- Use `supabase.auth.signUp({ email, password })` for email/password registration
- Use `supabase.auth.signInWithPassword({ email, password })` for login
- Use `supabase.auth.signInWithOAuth({ provider: 'google' })` for social auth
- Handle session management with `supabase.auth.getSession()` and `supabase.auth.onAuthStateChange()`
- Use `supabase.auth.signOut()` for logout
- Store user metadata in a public.profiles table, not in auth.users metadata
- Link profiles to auth.users via `id uuid references auth.users(id)`
- Use `auth.role()` for role-based access in RLS policies

### Real-time Subscriptions
- Enable replication on tables: `ALTER TABLE table_name REPLICA IDENTITY FULL`
- Subscribe to changes: `supabase.channel('name').on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, callback).subscribe()`
- Filter subscriptions: add `filter: 'user_id=eq.123'` to the subscription config
- Remove channels on component unmount: `supabase.removeChannel(channel)`
- Use broadcast for ephemeral messages: `channel.on('broadcast', { event: 'typing' }, callback)`
- Track presence: `channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState() })`
- Subscribe to presence: `channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { await channel.track({ user: name, online_at: new Date() }) } })`
- Enable Realtime on tables in the Supabase Dashboard > Database > Replication

### Queries
- Use `.select('column1, column2')` to limit returned fields
- Chain filters: `.eq('status', 'active').order('created_at', { ascending: false }).limit(10)`
- Use `.single()` to return a single object instead of an array (throws on zero/multiple rows)
- Use `.maybeSingle()` when zero results is acceptable (returns null)
- Use `.range(0, 9)` for pagination combined with `.order()`
- Join foreign tables: `.select('*, profiles(name, avatar_url)')`
- Use `.rpc('function_name', { param: value })` for calling Postgres functions

### Storage
- Create buckets with appropriate public/private access
- Upload files: `supabase.storage.from('avatars').upload('user-123/avatar.png', file)`
- Get public URL: `supabase.storage.from('avatars').getPublicUrl('user-123/avatar.png')`
- Use RLS on storage buckets to restrict access
- Delete files: `supabase.storage.from('avatars').remove(['user-123/avatar.png'])`
- Limit file sizes and types via bucket configuration
- Generate signed URLs for private files that need temporary access

### Migrations & CLI
- Use `supabase migration new <name>` to create timestamped migration files
- Write pure SQL in migration files — no ORM abstraction needed
- Apply migrations: `supabase db push` or `supabase migration up`
- Diff local schema against linked project: `supabase db diff --linked`
- Use `supabase link --project-ref <ref>` to connect to a remote project
- Run local Supabase: `supabase start` (requires Docker)
- Seed data: create `supabase/seed.sql` and run with `supabase db seed`

### Edge Functions
- Functions run on Deno runtime, deployed globally at the edge
- Create: `supabase functions new my-function` — generates `supabase/functions/my-function/index.ts`
- Serve function handles `Request`: `Deno.serve(async (req) => new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }))`
- Deploy: `supabase functions deploy my-function`
- Invoke from client: `supabase.functions.invoke('my-function', { body: { data } })`
- Access to service_role via `req.headers.get('Authorization')` — never expose in client
- Use `supabase functions serve` for local development with hot reload
- Environment variables set via `supabase secrets set KEY=VALUE`

Installation

Create supabase.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.

# Install Supabase CLI
brew install supabase/tap/supabase

# Start local development
supabase init
supabase start

# Generate TypeScript types
supabase gen types typescript --linked > src/types/supabase.ts

Examples

// supabase.ts — Typed Supabase client setup
import { createClient } from "@supabase/supabase-js";
import type { Database } from "./types/supabase";

const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
// auth.service.ts — Auth with typed profiles
import { supabase } from "./supabase";

export async function signUp(email: string, password: string, name: string) {
  const { data: authData, error: authError } = await supabase.auth.signUp({
    email,
    password,
  });

  if (authError) throw authError;

  const { error: profileError } = await supabase
    .from("profiles")
    .insert({ id: authData.user!.id, name });

  if (profileError) throw profileError;

  return authData;
}

export async function getCurrentProfile() {
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return null;

  const { data } = await supabase
    .from("profiles")
    .select("*")
    .eq("id", user.id)
    .single();

  return data;
}
// realtime.ts — Typed Realtime subscription
import { supabase } from "./supabase";
import type { Database } from "./types/supabase";

type Post = Database["public"]["Tables"]["posts"]["Row"];

export function subscribeToPosts(onChange: (post: Post) => void) {
  const channel = supabase
    .channel("posts-changes")
    .on<Post>(
      "postgres_changes",
      { event: "*", schema: "public", table: "posts" },
      (payload) => onChange(payload.new as Post),
    )
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}