Toolmingo
Guides11 min read

Supabase Cheat Sheet: The Open-Source Firebase Alternative

Complete Supabase reference — setup, auth, database queries, storage, realtime subscriptions, Edge Functions, and RLS policies. With TypeScript and Python examples.

Supabase is an open-source Firebase alternative built on top of PostgreSQL. It provides auth, realtime subscriptions, storage, and Edge Functions — all with a generous free tier. This cheat sheet covers everything you need to build production apps.

Quick reference

Task Code
Install JS client npm i @supabase/supabase-js
Create client createClient(url, anonKey)
Sign up auth.signUp({ email, password })
Sign in auth.signInWithPassword({ email, password })
Get user auth.getUser()
Select rows .from('table').select('*')
Insert row .from('table').insert({ col: val })
Update row .from('table').update({ col: val }).eq('id', id)
Delete row .from('table').delete().eq('id', id)
Upload file storage.from('bucket').upload(path, file)
Subscribe .channel('x').on('postgres_changes', ...)

Installation and setup

npm install @supabase/supabase-js
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

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

export const supabase = createClient(supabaseUrl, supabaseAnonKey)
# Python
from supabase import create_client
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)

Server-side client (with service role key)

import { createClient } from '@supabase/supabase-js'

// Only use on the server — service role bypasses RLS
export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

Authentication

Email + password

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secret123',
  options: {
    data: { full_name: 'Jane Doe' }  // stored in user_metadata
  }
})

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

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

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

// Get session (includes access token)
const { data: { session } } = await supabase.auth.getSession()

OAuth (Google, GitHub, etc.)

// Redirect to OAuth provider
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`
  }
})

Auth state listener

supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session?.user)
  } else if (event === 'SIGNED_OUT') {
    console.log('User signed out')
  } else if (event === 'TOKEN_REFRESHED') {
    console.log('Token refreshed')
  }
})

Password reset

// Send reset email
await supabase.auth.resetPasswordForEmail('user@example.com', {
  redirectTo: 'https://yourapp.com/reset-password'
})

// Update password (after user follows link)
await supabase.auth.updateUser({ password: 'newSecret123' })

Database queries

Supabase wraps PostgreSQL with a REST-like query builder. The JS client returns { data, error }.

SELECT

// All rows
const { data, error } = await supabase.from('posts').select('*')

// Specific columns
const { data } = await supabase
  .from('posts')
  .select('id, title, created_at')

// Join related table (foreign key)
const { data } = await supabase
  .from('posts')
  .select('id, title, author:profiles(name, avatar_url)')

// Many-to-many join
const { data } = await supabase
  .from('posts')
  .select('*, tags(name)')

// Count
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })

Filtering

// Equality
.eq('status', 'published')

// Inequality
.neq('status', 'draft')

// Comparison
.gt('views', 100)     // greater than
.gte('views', 100)    // greater than or equal
.lt('views', 1000)
.lte('views', 1000)

// Range
.gte('price', 10).lte('price', 50)

// In / not in
.in('status', ['published', 'featured'])
.not('status', 'in', '("draft","archived")')

// Like / ilike (case-insensitive)
.ilike('title', '%supabase%')

// Is null / not null
.is('deleted_at', null)
.not('deleted_at', 'is', null)

// Contains (for arrays and JSONB)
.contains('tags', ['typescript'])
.containedBy('tags', ['typescript', 'nextjs'])

// Full text search
.textSearch('content', 'supabase tutorial', { type: 'websearch' })

// OR conditions
.or('status.eq.published,status.eq.featured')

Pagination and ordering

// Order by column
const { data } = await supabase
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false })

// Limit and offset
const { data } = await supabase
  .from('posts')
  .select('*')
  .range(0, 9)    // rows 0 to 9 (10 results)

// Cursor-based pagination using a timestamp
const { data } = await supabase
  .from('posts')
  .select('*')
  .lt('created_at', lastCreatedAt)
  .order('created_at', { ascending: false })
  .limit(10)

INSERT, UPDATE, UPSERT, DELETE

// Insert single row
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'Hello', body: 'World', user_id: user.id })
  .select()   // return the inserted row

// Insert multiple rows
await supabase.from('tags').insert([
  { name: 'typescript' },
  { name: 'supabase' }
])

// Update
const { data, error } = await supabase
  .from('posts')
  .update({ title: 'Updated Title', updated_at: new Date().toISOString() })
  .eq('id', postId)
  .select()

// Upsert (insert or update on conflict)
await supabase.from('profiles').upsert(
  { id: user.id, full_name: 'Jane', updated_at: new Date().toISOString() },
  { onConflict: 'id' }
)

// Delete
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId)

Raw SQL (RPC — calling a Postgres function)

// Call a stored procedure / Postgres function
const { data, error } = await supabase
  .rpc('search_posts', { query_text: 'supabase', limit_count: 10 })
-- The Postgres function (defined in SQL editor or migration)
CREATE OR REPLACE FUNCTION search_posts(query_text text, limit_count int)
RETURNS TABLE(id uuid, title text, rank float)
LANGUAGE sql
AS $$
  SELECT id, title, ts_rank(search_vector, query) AS rank
  FROM posts, to_tsquery('english', query_text) query
  WHERE search_vector @@ query
  ORDER BY rank DESC
  LIMIT limit_count;
$$;

Row Level Security (RLS)

RLS is Supabase's permission system. Enable it per table, then write policies.

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

-- Read own posts only
CREATE POLICY "Users see own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

-- Read all published posts
CREATE POLICY "Anyone can read published posts"
  ON posts FOR SELECT
  USING (status = 'published');

-- Insert only as authenticated user
CREATE POLICY "Authenticated users can insert"
  ON posts FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

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

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

-- Admin bypass (use with service role key on server only)
CREATE POLICY "Service role has full access"
  ON posts
  USING (auth.role() = 'service_role');

Key functions inside RLS policies:

Function Description
auth.uid() UUID of the current user
auth.role() 'anon', 'authenticated', or 'service_role'
auth.jwt() Full JWT payload (access user_metadata, app_metadata)
auth.email() Current user's email

Storage

// Upload a file
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${user.id}/avatar.png`, file, {
    cacheControl: '3600',
    upsert: true
  })

// Get public URL (for public buckets)
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${user.id}/avatar.png`)
// data.publicUrl = 'https://...supabase.co/storage/v1/object/public/avatars/...'

// Create signed URL (for private buckets, expires in 60 seconds)
const { data, error } = await supabase.storage
  .from('documents')
  .createSignedUrl(`${user.id}/report.pdf`, 60)

// List files in a bucket
const { data, error } = await supabase.storage
  .from('avatars')
  .list(user.id, { limit: 20, sortBy: { column: 'created_at', order: 'desc' } })

// Download a file
const { data, error } = await supabase.storage
  .from('documents')
  .download(`${user.id}/report.pdf`)

// Delete a file
await supabase.storage.from('avatars').remove([`${user.id}/avatar.png`])

Storage bucket RLS (SQL)

-- Users can upload their own files
CREATE POLICY "Users upload own files"
  ON storage.objects FOR INSERT
  TO authenticated
  WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);

-- Users can read their own files
CREATE POLICY "Users read own files"
  ON storage.objects FOR SELECT
  TO authenticated
  USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);

Realtime subscriptions

// 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:', payload)
      // payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE'
      // payload.new: new row data
      // payload.old: old row data
    }
  )
  .subscribe()

// Subscribe to specific events with filter
const channel = supabase
  .channel('my-room')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: `room_id=eq.${roomId}`
    },
    (payload) => {
      setMessages((prev) => [...prev, payload.new as Message])
    }
  )
  .subscribe()

// Broadcast (custom events between clients)
const channel = supabase.channel('room-1')

// Send broadcast
await channel.send({
  type: 'broadcast',
  event: 'cursor-position',
  payload: { x: 100, y: 200, userId: user.id }
})

// Receive broadcast
channel.on('broadcast', { event: 'cursor-position' }, (payload) => {
  console.log('Cursor:', payload)
})

channel.subscribe()

// Unsubscribe
await supabase.removeChannel(channel)

Edge Functions

Edge Functions run on Deno at the edge, close to users.

// supabase/functions/hello/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const authHeader = req.headers.get('Authorization')!
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  )

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

  const body = await req.json()

  return new Response(
    JSON.stringify({ message: `Hello ${user.email}!`, data: body }),
    { headers: { 'Content-Type': 'application/json' } }
  )
})
# Deploy function
supabase functions deploy hello

# Invoke locally
supabase functions serve hello
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/hello' \
  --header 'Authorization: Bearer <ACCESS_TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{"name": "World"}'

CLI commands

# Install
npm install -g supabase

# Login
supabase login

# Init local project
supabase init

# Link to remote project
supabase link --project-ref <ref>

# Start local dev environment (Docker required)
supabase start

# Stop local environment
supabase stop

# Generate TypeScript types from DB schema
supabase gen types typescript --local > src/types/database.ts

# Create a new migration
supabase migration new add_profiles_table

# Apply migrations locally
supabase db push

# Pull remote schema changes
supabase db pull

# Reset local database
supabase db reset

# Inspect local DB
supabase db inspect

# View Edge Function logs
supabase functions logs hello

TypeScript type generation

Generate types once and get full autocomplete:

supabase gen types typescript --project-id <ref> > src/types/database.ts
// Use generated types
import { Database } from '@/types/database'

const supabase = createClient<Database>(url, key)

// Now queries are fully typed
const { data } = await supabase
  .from('posts')            // autocomplete table names
  .select('id, title')     // autocomplete column names

// data is typed as { id: string; title: string }[]

Next.js integration pattern

// lib/supabase/server.ts (Server Components)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createSupabaseServerClient() {
  const cookieStore = cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name) { return cookieStore.get(name)?.value },
        set(name, value, options) { cookieStore.set({ name, value, ...options }) },
        remove(name, options) { cookieStore.set({ name, value: '', ...options }) }
      }
    }
  )
}

// app/page.tsx (Server Component — no "use client")
export default async function Page() {
  const supabase = createSupabaseServerClient()
  const { data: posts } = await supabase
    .from('posts')
    .select('id, title')
    .order('created_at', { ascending: false })
    .limit(10)

  return <PostList posts={posts ?? []} />
}

Common mistakes

Mistake Fix
Using anon key on the server with admin operations Use SUPABASE_SERVICE_ROLE_KEY server-side only
Not enabling RLS on a table Run ALTER TABLE t ENABLE ROW LEVEL SECURITY;
Forgetting to check error in { data, error } Always check if (error) throw error
Storing service role key in client-side env (NEXT_PUBLIC_) Only expose NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY
Using supabase.from().select() without RLS policies All rows will be returned or blocked — write explicit policies
Not unsubscribing realtime channels on unmount Call supabase.removeChannel(channel) in cleanup
Calling createClient on every render Create client once per request (server) or as a singleton (client)
Forgetting .select() after insert/update to get returned data Chain .select() to get the row back

Supabase vs Firebase vs PlanetScale

Feature Supabase Firebase PlanetScale
Database PostgreSQL NoSQL (Firestore) MySQL (serverless)
Open source ✅ Yes ❌ No ❌ No
Self-hostable ✅ Yes ❌ No ❌ No
Auth ✅ Built-in ✅ Built-in ❌ None
Realtime ✅ Postgres CDC ✅ Firestore listeners ❌ None
Storage ✅ Built-in ✅ Cloud Storage ❌ None
Edge Functions ✅ Deno ✅ Cloud Functions ❌ None
TypeScript types ✅ Auto-generated ⚠️ Manual ✅ Auto-generated
SQL support ✅ Full SQL ❌ None ✅ Full SQL
Free tier rows Unlimited (500MB) 1GB Firestore 5GB

FAQ

Q: Can I use Supabase with any framework?
Yes. Supabase has official clients for JavaScript/TypeScript, Python, Dart (Flutter), Swift, Kotlin, and C#. The REST API works with any HTTP client. The @supabase/ssr package handles cookies for Next.js, SvelteKit, Remix, Nuxt, and other SSR frameworks.

Q: What is the difference between anon and service_role keys?
The anon key is public and respects RLS policies — safe to expose in the browser. The service_role key bypasses RLS entirely and has full admin access. Never expose the service role key client-side. Use it only in server-side code (API routes, Edge Functions, cron jobs).

Q: How does Supabase realtime work?
Supabase uses PostgreSQL logical replication (the wal2json plugin) to stream database changes over WebSockets. You subscribe to postgres_changes events filtered by table, event type, and column values. Realtime also supports Broadcast (arbitrary events between clients) and Presence (online status tracking).

Q: How do I handle auth in Next.js App Router?
Use @supabase/ssr which provides createServerClient for Server Components and Route Handlers, and createBrowserClient for Client Components. The library manages session cookies automatically through middleware. Run supabase.auth.getUser() on the server — never rely on getSession() alone, which is unauthenticated.

Q: Can I use Supabase locally without internet?
Yes. supabase start spins up a complete local stack via Docker: Postgres, PostgREST, GoTrue (auth), Realtime, Storage, and the Studio dashboard at http://localhost:54323. Your local environment is an exact mirror of production.

Q: How do I migrate from Firebase to Supabase?
Export Firestore documents as JSON, then transform and import into Postgres tables. Migrate Firebase Auth users via the Supabase Admin API (supabase.auth.admin.createUser). Replace Firestore listeners with Supabase realtime subscriptions. The data model changes from document collections to relational tables, which enables SQL queries, joins, and transactions.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools