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
Ruby Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Ruby that enforce Rails best practices, modern Ruby 3+ features, and clean code principles with AI assistance for secure, maintainable, production-ready applications.
NestJS Cursor Rules: Enterprise Node.js Framework
Cursor rules for NestJS covering modules, dependency injection, controllers/providers, guards/pipes/interceptors, DTO validation, OpenAPI, and Jest testing.
Bash/Shell Cursor Rules: Scripting and Automation Guide
Cursor rules for Bash and shell scripting covering POSIX compliance, error handling, command-line tools, and automation patterns for efficient terminal workflows.