All topics
Backend · Learning hub

Supabase notes for developers

Master Supabase with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
Supabase

Supabase Essentials

Supabase Essentials Client Setup & Auth import { createClient } from '@supabase/supabase-js'; import type { Database } from './database.types'; // generated typ

Supabase Essentials

Client Setup & Auth

import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types'; // generated types

const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// Auth
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123',
  options: { data: { full_name: 'Alice' } },
});

const { error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password123',
});

await supabase.auth.signInWithOAuth({ provider: 'github' });
await supabase.auth.signOut();

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

// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') console.log('User signed in', session?.user);
  if (event === 'SIGNED_OUT') console.log('User signed out');
});

Database Queries

// SELECT
const { data, error } = await supabase
  .from('posts')
  .select('id, title, body, created_at, author:users(name, email)')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .range(0, 19);   // pagination: rows 0-19

// Filters
.eq('id', userId)
.neq('status', 'deleted')
.gt('views', 100)
.gte('age', 18)
.in('role', ['admin', 'moderator'])
.like('name', '%alice%')
.ilike('name', '%alice%')          // case-insensitive
.is('deleted_at', null)
.not('status', 'eq', 'hidden')
.or('role.eq.admin,role.eq.moderator')

// INSERT
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'Hello', body: 'World', author_id: user.id })
  .select()
  .single();

// UPSERT
await supabase
  .from('profiles')
  .upsert({ id: user.id, username: 'alice' }, { onConflict: 'id' });

// UPDATE
await supabase
  .from('posts')
  .update({ status: 'published' })
  .eq('id', postId)
  .eq('author_id', userId);  // RLS-style safety

// DELETE
await supabase
  .from('posts')
  .delete()
  .eq('id', postId);

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

Row Level Security & Realtime

-- Row Level Security (RLS) — SQL policies
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy: users can only read their own posts
CREATE POLICY "users read own posts"
  ON posts FOR SELECT
  USING (auth.uid() = author_id);

-- Policy: users can insert posts as themselves
CREATE POLICY "users insert own posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = author_id);

-- Policy: everyone can read published posts
CREATE POLICY "public read published"
  ON posts FOR SELECT
  USING (status = 'published');

-- Policy: admins can do anything
CREATE POLICY "admins full access"
  ON posts
  USING (auth.jwt() ->> 'role' = 'admin');
// Realtime subscriptions
const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'posts', filter: `author_id=eq.${userId}` },
    (payload) => {
      if (payload.eventType === 'INSERT') addPost(payload.new);
      if (payload.eventType === 'UPDATE') updatePost(payload.new);
      if (payload.eventType === 'DELETE') removePost(payload.old.id);
    }
  )
  .subscribe();

// Cleanup
supabase.removeChannel(channel);

// Storage
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.png`, file, { upsert: true });

const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/avatar.png`);
Supabase

Supabase Fundamentals

Supabase Fundamentals Supabase is an open-source Firebase alternative. It provides a PostgreSQL database, Auth, realtime subscriptions, storage, and edge functi

Supabase Fundamentals

Supabase is an open-source Firebase alternative. It provides a PostgreSQL database, Auth, realtime subscriptions, storage, and edge functions — all accessible via a type-safe JavaScript client.

Client Setup & Type Generation

# Install
npm install @supabase/supabase-js

# For Next.js (SSR/SSG with cookie-based auth)
npm install @supabase/ssr

# Generate TypeScript types from your database schema
npx supabase login
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/types/database.types.ts

# Or from local Supabase dev instance
npx supabase gen types typescript --local > src/types/database.types.ts
// src/lib/supabase.ts — browser client
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@/types/database.types';

export const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// src/lib/supabase-server.ts — server-side client (Next.js App Router)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export function createSupabaseServerClient() {
  const cookieStore = cookies();
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll(); },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          );
        },
      },
    }
  );
}

Authentication — Email, OAuth, Magic Link

// Email + password
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123',
  options: {
    data: { full_name: 'Alice Smith', role: 'user' },  // stored in raw_user_meta_data
    emailRedirectTo: 'https://myapp.com/auth/callback',
  },
});

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password123',
});

// OAuth providers (GitHub, Google, Discord, etc.)
await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://myapp.com/auth/callback',
    scopes: 'read:user user:email',
  },
});

// Magic link (passwordless)
await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: { emailRedirectTo: 'https://myapp.com/auth/callback' },
});

// Phone OTP
await supabase.auth.signInWithOtp({ phone: '+1234567890' });
await supabase.auth.verifyOtp({ phone: '+1234567890', token: '123456', type: 'sms' });

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

// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  // events: SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED, PASSWORD_RECOVERY
  if (event === 'SIGNED_IN') console.log('User:', session?.user);
});

await supabase.auth.signOut();

Database Queries

// SELECT with joins
const { data: posts, error } = await supabase
  .from('posts')
  .select(`
    id, title, body, created_at,
    author:profiles(id, username, avatar_url),
    tags(name)
  `)
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .range(0, 19)                       // pagination (rows 0–19)
  .returns<PostWithAuthor[]>();        // narrow the inferred type

// Filters
.eq('id', userId)                     // WHERE id = $1
.neq('status', 'deleted')             // WHERE status != $1
.gt('views', 100) / .gte('views', 100)
.in('role', ['admin', 'moderator'])
.ilike('name', '%alice%')             // case-insensitive LIKE
.is('deleted_at', null)
.not('status', 'eq', 'hidden')
.or('role.eq.admin,role.eq.moderator')
.filter('metadata->country', 'eq', 'US')   // JSONB field

// INSERT — returns inserted row
const { data: post, error } = await supabase
  .from('posts')
  .insert({ title: 'Hello', body: 'World', author_id: user.id })
  .select()
  .single();

// UPSERT
await supabase
  .from('profiles')
  .upsert({ id: user.id, username: 'alice', updated_at: new Date().toISOString() },
    { onConflict: 'id' });

// UPDATE
await supabase
  .from('posts')
  .update({ status: 'published', published_at: new Date().toISOString() })
  .eq('id', postId)
  .eq('author_id', user.id);         // always scope to current user

// DELETE
await supabase.from('posts').delete().eq('id', postId);

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

Row Level Security (RLS)

-- Enable RLS — without this, all rows are accessible to anon
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- SELECT: users read only their own posts
CREATE POLICY "users_read_own_posts"
  ON posts FOR SELECT
  USING (auth.uid() = author_id);

-- SELECT: anyone reads published posts
CREATE POLICY "public_read_published"
  ON posts FOR SELECT
  USING (status = 'published');

-- INSERT: author_id must match the authenticated user
CREATE POLICY "users_insert_own_posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = author_id);

-- UPDATE: only author can update their own post
CREATE POLICY "users_update_own_posts"
  ON posts FOR UPDATE
  USING (auth.uid() = author_id)
  WITH CHECK (auth.uid() = author_id);

-- Admin bypass — check custom claim from JWT metadata
CREATE POLICY "admins_full_access"
  ON posts
  USING ((auth.jwt() ->> 'user_metadata')::jsonb ->> 'role' = 'admin');

-- Helper function — reuse in multiple policies
CREATE FUNCTION is_admin() RETURNS boolean AS $$
  SELECT (auth.jwt() ->> 'user_metadata')::jsonb ->> 'role' = 'admin';
$$ LANGUAGE sql STABLE;
Supabase

Realtime & Storage

Realtime & Storage Supabase Realtime broadcasts database changes over WebSockets. Storage provides S3-compatible file storage with RLS policies. Edge Functions

Realtime & Storage

Supabase Realtime broadcasts database changes over WebSockets. Storage provides S3-compatible file storage with RLS policies. Edge Functions run Deno-based serverless code close to users.

Realtime Subscriptions

// Subscribe to database changes (requires replication enabled on the table)
const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    {
      event: '*',                     // INSERT | UPDATE | DELETE | *
      schema: 'public',
      table: 'posts',
      filter: `author_id=eq.${userId}`,  // server-side filter
    },
    (payload) => {
      switch (payload.eventType) {
        case 'INSERT': addPost(payload.new as Post); break;
        case 'UPDATE': updatePost(payload.new as Post); break;
        case 'DELETE': removePost((payload.old as Post).id); break;
      }
    }
  )
  .subscribe((status, err) => {
    if (status === 'SUBSCRIBED') console.log('Realtime connected');
    if (status === 'CHANNEL_ERROR') console.error('Realtime error', err);
  });

// Cleanup on unmount
return () => { supabase.removeChannel(channel); };

// Broadcast — send custom events to other clients in the same channel
const presenceChannel = supabase.channel('room:lobby');
presenceChannel
  .on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
    updateCursor(payload.userId, payload.x, payload.y);
  })
  .subscribe();

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

Presence — Track Online Users

// Presence — know who is online in real time
const room = supabase.channel('room:project-1', {
  config: { presence: { key: user.id } },
});

room
  .on('presence', { event: 'sync' }, () => {
    const state = room.presenceState<{ name: string; avatar: string }>();
    const onlineUsers = Object.values(state).flat();
    setOnlineUsers(onlineUsers);
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('User joined:', key, newPresences);
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('User left:', key, leftPresences);
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await room.track({ name: user.name, avatar: user.avatar });
    }
  });

// Untrack when leaving
await room.untrack();

Storage — File Uploads & Access

// Upload a file
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.webp`, file, {
    contentType: 'image/webp',
    upsert: true,               // overwrite if exists
    cacheControl: '3600',       // Cache-Control max-age
  });

// Get public URL (bucket must be set to public)
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/avatar.webp`);

// Signed URL (for private buckets, expires after N seconds)
const { data: { signedUrl } } = await supabase.storage
  .from('private-docs')
  .createSignedUrl(`${userId}/report.pdf`, 3600);

// List files in a folder
const { data: files } = await supabase.storage
  .from('avatars')
  .list(userId, { limit: 20, offset: 0, sortBy: { column: 'created_at', order: 'desc' } });

// Delete
await supabase.storage.from('avatars').remove([`${userId}/old-avatar.webp`]);

// Move / rename
await supabase.storage.from('avatars').move(`${userId}/temp.png`, `${userId}/avatar.png`);

// Storage RLS — in Supabase dashboard SQL editor
// CREATE POLICY "user_owns_avatar"
//   ON storage.objects FOR ALL
//   USING (auth.uid()::text = (storage.foldername(name))[1]);

Edge Functions & Database Functions

// supabase/functions/send-email/index.ts — Deno edge function
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!   // service role bypasses RLS
  );

  const { to, subject, body } = await req.json();
  // ... call Resend, SendGrid, etc.

  return new Response(JSON.stringify({ ok: true }), {
    headers: { 'Content-Type': 'application/json' },
  });
});

// Deploy
// npx supabase functions deploy send-email

// Call from client
const { data, error } = await supabase.functions.invoke('send-email', {
  body: { to: 'alice@example.com', subject: 'Welcome!', body: 'Hello' },
});

// Database function + trigger — auto-create profile on signup
-- CREATE FUNCTION public.handle_new_user()
-- RETURNS trigger AS $$
-- BEGIN
--   INSERT INTO public.profiles (id, email, created_at)
--   VALUES (new.id, new.email, now());
--   RETURN new;
-- END;
-- $$ LANGUAGE plpgsql SECURITY DEFINER;
--
-- CREATE TRIGGER on_auth_user_created
--   AFTER INSERT ON auth.users
--   FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

Keep your Supabase knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever