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.

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;
}
-- 20240101000000_profiles.sql — RLS migration
create table public.profiles (
id uuid references auth.users(id) on delete cascade primary key,
name text not null,
avatar_url text,
created_at timestamptz not null default now()
);
alter table public.profiles enable row level security;
create policy "Users can view own profile"
on public.profiles for select
using (auth.uid() = id);
create policy "Users can update own profile"
on public.profiles for update
using (auth.uid() = id);
create policy "Users can insert own profile"
on public.profiles for insert
with check (auth.uid() = id);
// 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);
};
}
Related Resources
Related Articles
Nuxt Cursor Rules: Vue Full-Stack Framework Guide
Cursor rules for Nuxt development covering server-side rendering, auto-imports, modules, composables, and deployment strategies for universal Vue applications.
React Native Cursor Rules: Mobile Best Practices Guide
Professional React Native cursor rules guiding component architecture, TypeScript, navigation, testing with Jest and Detox, and performance optimization for cross‑platform iOS and Android apps.
Laravel Cursor Rules: PHP Web Application Guide
Cursor rules for Laravel development covering Eloquent ORM, Artisan commands, service providers, queue management, and testing for maintainable PHP applications.