Complete Supabase Guide for Developers
ID | EN

Complete Supabase Guide for Developers

Thursday, Dec 25, 2025

If you’ve ever used Firebase and felt “this seems too vendor-locked,” welcome to the club. Supabase is here as a powerful open-source alternative, and most importantly: built on PostgreSQL—a database that’s been battle-tested for decades.

In this article, I’ll cover everything you need to know about Supabase. From setup to production-ready. Let’s go!

What is Supabase?

Supabase is an open-source Backend-as-a-Service (BaaS) that provides:

  • PostgreSQL Database - Full Postgres with a beautiful GUI
  • Authentication - Email, OAuth, Magic Link, Phone Auth
  • Storage - Object storage for files and media
  • Edge Functions - Serverless functions using Deno
  • Realtime - Subscribe to database changes live
  • Vector/AI - For AI applications with pgvector

What makes Supabase attractive is you can self-host if you want, or use their managed service. Your data, your rules.

Setting Up a Supabase Project

1. Create Account and Project

First, sign up at supabase.com and create a new project:

  1. Click “New Project”
  2. Choose organization (or create new)
  3. Fill in project name and database password
  4. Choose region (Singapore is closest for Asia-Pacific)
  5. Wait a few minutes until provisioning completes

2. Get API Keys

After project is ready, get credentials at Settings > API:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxxxxx
SUPABASE_SERVICE_ROLE_KEY=eyJxxxxxx  # Don't expose to client!

Important: ANON_KEY is safe to expose to browser because it’s restricted by Row Level Security. SERVICE_ROLE_KEY bypasses all security, so keep it safe on server only.

Database: Tables, Relationships, and RLS

Creating Tables

You can create tables via GUI in dashboard or use SQL Editor:

-- Create profiles table
CREATE TABLE profiles (
  id UUID REFERENCES auth.users ON DELETE CASCADE PRIMARY KEY,
  username TEXT UNIQUE,
  full_name TEXT,
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Create posts table
CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for performance
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = TRUE;

Relationships

Supabase auto-detects foreign keys and creates relationships. Queries with joins become easy:

const { data, error } = await supabase
  .from('posts')
  .select(`
    *,
    profiles (
      username,
      avatar_url
    )
  `)
  .eq('published', true)
  .order('created_at', { ascending: false });

Row Level Security (RLS)

RLS is a game-changer. You define security rules at database level, not in application code:

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy: Everyone can read published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = TRUE);

-- Policy: User can only CRUD their own posts
CREATE POLICY "Users can manage their own posts"
ON posts FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- Policy: User can read their own posts (including drafts)
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);

Tip: Always test RLS policies using “SQL Editor” with anon or authenticated role.

Authentication

Supabase Auth supports various methods. All built-in, just enable.

Email/Password

// Sign Up
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'super-secret-password',
});

// Sign In
const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'super-secret-password',
});

// Sign Out
await supabase.auth.signOut();

OAuth (Google, GitHub, etc.)

Enable provider in dashboard (Authentication > Providers), then:

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://yourapp.com/auth/callback',
  },
});

Perfect for passwordless experience:

const { data, error } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    emailRedirectTo: 'https://yourapp.com/welcome',
  },
});

Handle Auth State

// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
  console.log(event, session);
  
  if (event === 'SIGNED_IN') {
    // Redirect to dashboard
  } else if (event === 'SIGNED_OUT') {
    // Redirect to login
  }
});

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

Storage: Upload and Manage Files

Supabase Storage is similar to S3 but simpler. Perfect for avatars, images, documents, etc.

Setup Bucket

Create bucket in dashboard or via SQL:

-- Create public bucket for avatars
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', TRUE);

-- Create private bucket for documents
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', FALSE);

Storage Policies

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

-- Policy: Avatars can be viewed by everyone
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

Upload Files

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

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

// Download file (for private buckets)
const { data, error } = await supabase.storage
  .from('documents')
  .download('path/to/file.pdf');

Edge Functions

Edge Functions are serverless functions that run on Deno. Perfect for:

  • Webhooks
  • Third-party API calls
  • Complex business logic
  • Scheduled jobs

Create Function

# Install Supabase CLI
npm install -g supabase

# Init and create function
supabase init
supabase functions new hello-world

Code Function

// supabase/functions/hello-world/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

serve(async (req) => {
  try {
    const { name } = await req.json();
    
    // Access Supabase from function
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
    );
    
    const { data, error } = await supabase
      .from('greetings')
      .insert({ message: `Hello ${name}!` })
      .select()
      .single();
    
    return new Response(
      JSON.stringify({ data }),
      { headers: { "Content-Type": "application/json" } }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500 }
    );
  }
});

Deploy and Invoke

# Deploy
supabase functions deploy hello-world

# Test locally
supabase functions serve hello-world
// Invoke from client
const { data, error } = await supabase.functions.invoke('hello-world', {
  body: { name: 'John' },
});

Realtime Subscriptions

Supabase’s killer feature. Subscribe to database changes in real-time.

Subscribe to Table Changes

// Subscribe to all changes in posts table
const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    {
      event: '*', // INSERT, UPDATE, DELETE, or *
      schema: 'public',
      table: 'posts',
    },
    (payload) => {
      console.log('Change received!', payload);
      
      if (payload.eventType === 'INSERT') {
        // Handle new post
      } else if (payload.eventType === 'UPDATE') {
        // Handle updated post
      } else if (payload.eventType === 'DELETE') {
        // Handle deleted post
      }
    }
  )
  .subscribe();

// Unsubscribe
supabase.removeChannel(channel);

Filter Realtime Events

// Only subscribe to posts from specific user
const channel = supabase
  .channel('my-posts')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts',
      filter: `user_id=eq.${userId}`,
    },
    handleNewPost
  )
  .subscribe();

Broadcast and Presence

Besides database changes, you can also create custom channels:

// Presence - track online users
const channel = supabase.channel('online-users');

channel
  .on('presence', { event: 'sync' }, () => {
    const state = channel.presenceState();
    console.log('Online users:', Object.keys(state).length);
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({ user_id: userId, online_at: new Date() });
    }
  });

Integration with Next.js

Install Dependencies

npm install @supabase/supabase-js @supabase/ssr

Setup Client

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Handle server component
          }
        },
      },
    }
  );
}

Middleware for Auth

// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: { headers: request.headers },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          );
          response = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  const { data: { user } } = await supabase.auth.getUser();

  // Protect routes
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Server Component Example

// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server';

export default async function PostsPage() {
  const supabase = await createClient();
  
  const { data: posts } = await supabase
    .from('posts')
    .select('*, profiles(username)')
    .eq('published', true)
    .order('created_at', { ascending: false });

  return (
    <div>
      {posts?.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.profiles.username}</p>
        </article>
      ))}
    </div>
  );
}

Best Practices and Tips

1. Always Use RLS

Never disable RLS in production. Even for admin operations, better to use service role key on server than disable RLS.

2. Optimize Queries

// ❌ Bad: Fetch all columns
const { data } = await supabase.from('posts').select('*');

// ✅ Good: Fetch only what's needed
const { data } = await supabase.from('posts').select('id, title, created_at');

3. Handle Errors Properly

const { data, error } = await supabase.from('posts').select();

if (error) {
  console.error('Error fetching posts:', error.message);
  // Handle error appropriately
  return;
}

// Safe to use data here

4. Use Database Functions for Complex Logic

-- Create function to increment view count atomically
CREATE OR REPLACE FUNCTION increment_view_count(post_id UUID)
RETURNS void AS $$
BEGIN
  UPDATE posts SET view_count = view_count + 1 WHERE id = post_id;
END;
$$ LANGUAGE plpgsql;
await supabase.rpc('increment_view_count', { post_id: 'xxx' });

5. Setup Database Triggers

-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER posts_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at();

6. Use Connection Pooling

For high-traffic apps, use the pooler connection string (port 6543) instead of direct connection.

Pricing and Free Tier

Supabase has a generous free tier:

FeatureFree TierPro ($25/mo)
Database500MB8GB
Storage1GB100GB
Bandwidth2GB250GB
Edge Functions500K invocations2M invocations
Auth50K MAU100K MAU
Realtime200 concurrent500 concurrent

Free tier is great for:

  • Side projects
  • MVP and prototyping
  • Learning and experimenting

Upgrade to Pro when you start scaling or need features like:

  • Daily backups
  • Email support
  • More resources

Conclusion

Supabase is a solid choice for modern backend. With PostgreSQL as the foundation, you get:

  • Reliability - PostgreSQL has been proven in production for decades
  • Flexibility - Full SQL access, can self-host, no vendor lock-in
  • Developer Experience - Nice dashboard, intuitive SDK
  • Cost Effective - Generous free tier, transparent pricing

If you’re coming from Firebase, transitioning to Supabase is relatively smooth. Concepts are similar, but with the power of SQL and open-source ecosystem.

Start exploring at supabase.com and join their active Discord community. Happy building! 🚀