TechSetupGuides
Intermediatesupabasepostgresqldatabasebackendbaasfirebase-alternativeauthreal-timestorage

Supabase: Open Source Firebase Alternative

Set up Supabase, the open source PostgreSQL-based backend platform with REST/GraphQL APIs, real-time subscriptions, authentication, and storage.

  1. Step 1

    Architecture Overview

    Supabase is a modular Backend-as-a-Service built on PostgreSQL. The platform consists of several microservices that work together: PostgREST for auto-generated REST APIs, GoTrue for authentication, Realtime for WebSocket subscriptions, Storage API for file storage, Kong as an API gateway, and pg_graphql for GraphQL support. Each component is open source and can be self-hosted or used via Supabase's cloud offering.

  2. Step 2

    Quick Start with Supabase Cloud

    The fastest way to get started is using Supabase's hosted platform. Create a free account and provision a new project. You'll get a dedicated PostgreSQL database, API endpoints, and authentication configured automatically.

    # Visit https://supabase.com/dashboard and:
    # 1. Sign up / log in
    # 2. Click 'New project'
    # 3. Choose organization, name, password, and region
    # 4. Wait ~2 minutes for provisioning
    
    # Your project URL and keys will be available in Settings > API
  3. Step 3

    Install the Supabase CLI

    The Supabase CLI enables local development with a full Supabase stack running in Docker. It manages migrations, generates TypeScript types, and syncs with your cloud project.

    # Install via npm (requires Node.js 18+)
    npm install -g supabase
    
    # Or via Homebrew on macOS
    brew install supabase/tap/supabase
    
    # Or via Scoop on Windows
    scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
    scoop install supabase
    
    # Verify installation
    supabase --version
  4. Step 4

    Initialize a local Supabase project

    Initialize Supabase in your project directory. This creates a supabase/ folder with configuration files and a migrations directory.

    # Navigate to your project root
    cd your-project
    
    # Initialize Supabase (creates supabase/ directory)
    supabase init
    
    # Link to your cloud project (optional, for syncing)
    supabase link --project-ref <your-project-ref>
  5. Step 5

    Start the local Supabase stack

    Start all Supabase services locally using Docker. This spins up PostgreSQL, PostgREST, GoTrue, Realtime, Storage, Kong gateway, and the Supabase Studio UI.

    # Start all services (requires Docker)
    supabase start
    
    # Services will be available at:
    # - API: http://localhost:54321
    # - Studio: http://localhost:54323
    # - Database: postgresql://postgres:postgres@localhost:54322/postgres
    
    # Stop services when done
    supabase stop
  6. Step 6

    Create your first table with RLS

    Create a migration file and define a table with Row Level Security (RLS) enabled. RLS policies control data access at the row level based on user identity.

    -- Generate a new migration
    -- supabase migration new create_todos
    
    -- In the generated migration file:
    create table todos (
      id uuid primary key default gen_random_uuid(),
      user_id uuid references auth.users(id) on delete cascade not null,
      title text not null,
      is_complete boolean default false,
      created_at timestamptz default now()
    );
    
    -- Enable RLS
    alter table todos enable row level security;
    
    -- Policy: users can only see their own todos
    create policy "Users can view their own todos"
      on todos for select
      using (auth.uid() = user_id);
    
    -- Policy: users can insert their own todos
    create policy "Users can insert their own todos"
      on todos for insert
      with check (auth.uid() = user_id);
    
    -- Policy: users can update their own todos
    create policy "Users can update their own todos"
      on todos for update
      using (auth.uid() = user_id);
  7. Step 7

    Apply migrations

    Run your migrations to update the local database schema. The CLI tracks which migrations have been applied via the supabase_migrations.schema_migrations table.

    # Apply all pending migrations to local DB
    supabase db push
    
    # Reset local DB from scratch (useful for development)
    supabase db reset
    
    # Push migrations to linked cloud project
    supabase db push --linked
  8. Step 8

    Install the Supabase JavaScript client

    Install the official Supabase JavaScript client library for interacting with your database, auth, storage, and real-time subscriptions from the browser or Node.js.

    npm install @supabase/supabase-js
  9. Step 9

    Initialize the Supabase client

    Create a client instance with your project URL and anon key. The anon key is safe to expose in browser code — Row Level Security policies enforce access control.

    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)
  10. Step 10

    Query data with the Supabase client

    Use the auto-generated REST API through the JavaScript client. All CRUD operations respect your RLS policies — users automatically only see/modify their own data.

    // Fetch all todos for the authenticated user
    const { data: todos, error } = await supabase
      .from('todos')
      .select('*')
      .order('created_at', { ascending: false })
    
    // Insert a new todo
    const { data, error } = await supabase
      .from('todos')
      .insert({ title: 'Learn Supabase', user_id: user.id })
      .select()
      .single()
    
    // Update a todo
    const { error } = await supabase
      .from('todos')
      .update({ is_complete: true })
      .eq('id', todoId)
    
    // Delete a todo
    const { error } = await supabase
      .from('todos')
      .delete()
      .eq('id', todoId)
  11. Step 11

    Set up authentication

    Supabase Auth (powered by GoTrue) supports multiple providers: email/password, magic links, OAuth (GitHub, Google, etc.), and phone auth. Configure providers in the Supabase dashboard.

    // Sign up with email and password
    const { data, error } = await supabase.auth.signUp({
      email: 'user@example.com',
      password: 'secure-password'
    })
    
    // Sign in with email and password
    const { data, error } = await supabase.auth.signInWithPassword({
      email: 'user@example.com',
      password: 'secure-password'
    })
    
    // Sign in with OAuth (GitHub example)
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'github'
    })
    
    // Get current user session
    const { data: { session } } = await supabase.auth.getSession()
    
    // Sign out
    const { error } = await supabase.auth.signOut()
  12. Step 12

    Listen to auth state changes

    Subscribe to authentication state changes to update your UI when users sign in or out. The listener fires whenever the session changes.

    // Set up 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('Session token refreshed')
      }
    })
    
    // The listener returns an unsubscribe function
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => { /* ... */ }
    )
    
    // Clean up when component unmounts
    subscription.unsubscribe()
  13. Step 13

    Subscribe to real-time database changes

    Enable real-time subscriptions on your tables to receive instant updates when data changes. Supabase Realtime uses PostgreSQL's replication feature and WebSockets.

    // Subscribe to all changes on the todos table
    const channel = supabase
      .channel('todos-changes')
      .on(
        'postgres_changes',
        {
          event: '*',  // 'INSERT', 'UPDATE', 'DELETE', or '*' for all
          schema: 'public',
          table: 'todos'
        },
        (payload) => {
          console.log('Change detected:', payload)
          // payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE'
          // payload.new: new row data (for INSERT/UPDATE)
          // payload.old: old row data (for UPDATE/DELETE)
        }
      )
      .subscribe()
    
    // Filter changes by specific criteria
    const userChannel = supabase
      .channel('my-todos')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'todos',
          filter: `user_id=eq.${userId}`
        },
        (payload) => { /* ... */ }
      )
      .subscribe()
    
    // Unsubscribe when done
    channel.unsubscribe()
    ⚠ Heads up: Real-time requires replication to be enabled on your table. Run `alter table todos replica identity full;` in your migration.
  14. Step 14

    Upload and manage files with Storage

    Supabase Storage provides S3-compatible object storage with access control tied to your RLS policies. Create buckets and upload files from the client.

    // Upload a file to a bucket
    const file = event.target.files[0]
    const { data, error } = await supabase.storage
      .from('avatars')
      .upload(`public/${user.id}/avatar.png`, file, {
        cacheControl: '3600',
        upsert: true
      })
    
    // Get public URL for a file
    const { data: publicUrlData } = supabase.storage
      .from('avatars')
      .getPublicUrl(`public/${user.id}/avatar.png`)
    
    const publicUrl = publicUrlData.publicUrl
    
    // Download a file
    const { data: blob, error } = await supabase.storage
      .from('avatars')
      .download(`public/${user.id}/avatar.png`)
    
    // List files in a folder
    const { data: files, error } = await supabase.storage
      .from('avatars')
      .list(`public/${user.id}`, {
        limit: 100,
        offset: 0,
        sortBy: { column: 'name', order: 'asc' }
      })
    
    // Delete files
    const { data, error } = await supabase.storage
      .from('avatars')
      .remove([`public/${user.id}/avatar.png`])
  15. Step 15

    Create storage buckets with RLS policies

    Storage buckets can have custom access policies. Create buckets via the dashboard or SQL, then define policies similar to table RLS.

    -- Create a storage bucket
    insert into storage.buckets (id, name, public)
    values ('avatars', 'avatars', false);
    
    -- Policy: users 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: anyone can view avatars
    create policy "Avatars are publicly accessible"
      on storage.objects for select
      using (bucket_id = 'avatars');
    
    -- Policy: users can update their own avatar
    create policy "Users can update their own avatar"
      on storage.objects for update
      using (
        bucket_id = 'avatars' AND
        auth.uid()::text = (storage.foldername(name))[1]
      );
  16. Step 16

    Use Database Functions and Triggers

    PostgreSQL functions and triggers give you server-side logic that runs automatically on data changes. Common use cases: denormalized counters, computed columns, audit logs, and validation.

    -- Example: Auto-update a 'updated_at' timestamp
    create or replace function update_updated_at_column()
    returns trigger
    language plpgsql
    as $$
    begin
      new.updated_at = now();
      return new;
    end;
    $$;
    
    create trigger update_todos_updated_at
      before update on todos
      for each row
      execute function update_updated_at_column();
    
    -- Example: Increment a counter on insert
    create or replace function increment_todo_count()
    returns trigger
    language plpgsql
    security definer set search_path = public
    as $$
    begin
      update profiles
      set todo_count = todo_count + 1
      where id = new.user_id;
      return new;
    end;
    $$;
    
    create trigger increment_todo_count_trigger
      after insert on todos
      for each row
      execute function increment_todo_count();
    ⚠ Heads up: Always use `security definer set search_path = public` on trigger functions to prevent privilege escalation attacks.
  17. Step 17

    Generate TypeScript types from your schema

    Auto-generate TypeScript definitions for type-safe queries. The CLI introspects your database schema and outputs a types file.

    # Generate types from local database
    supabase gen types typescript --local > types/supabase.ts
    
    # Generate types from linked cloud project
    supabase gen types typescript --linked > types/supabase.ts
  18. Step 18

    Use generated types with the client

    Import the generated types and pass them to the client for full type safety on queries, inserts, and updates.

    import { createClient } from '@supabase/supabase-js'
    import type { Database } from './types/supabase'
    
    const supabase = createClient<Database>(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    )
    
    // Now queries are fully typed
    const { data: todos } = await supabase
      .from('todos')  // 'todos' is autocompleted
      .select('*')    // return type is inferred as Todo[]
    
    // Insert with type checking
    const { error } = await supabase
      .from('todos')
      .insert({
        title: 'Typed todo',
        user_id: userId,
        is_complete: false
      })  // TypeScript errors if fields are missing/wrong type
  19. Step 19

    Call PostgreSQL functions (RPC)

    Expose custom PostgreSQL functions as API endpoints. Useful for complex queries, aggregations, or operations that don't map to simple CRUD.

    -- Create a function in your migration
    create or replace function get_completed_todos_count(p_user_id uuid)
    returns bigint
    language sql
    security definer set search_path = public
    as $$
      select count(*)
      from todos
      where user_id = p_user_id and is_complete = true;
    $$;
  20. Step 20

    Call RPC from the client

    Invoke PostgreSQL functions from the client using .rpc(). Parameters are type-checked if you're using generated types.

    // Call the function from the client
    const { data, error } = await supabase
      .rpc('get_completed_todos_count', {
        p_user_id: userId
      })
    
    if (!error) {
      console.log('Completed todos:', data)
    }
  21. Step 21

    Enable full-text search

    PostgreSQL's built-in full-text search is powerful. Add a generated tsvector column and a GIN index for fast text queries.

    -- Add a search vector column
    alter table todos add column search_vector tsvector
      generated always as (
        to_tsvector('english', coalesce(title, ''))
      ) stored;
    
    -- Create GIN index for fast search
    create index todos_search_idx on todos using gin(search_vector);
    
    -- Query from the client
    -- Use .textSearch() for type-safe full-text queries
    const { data } = await supabase
      .from('todos')
      .select('*')
      .textSearch('search_vector', 'learn & supabase')
  22. Step 22

    Self-host Supabase with Docker

    Run the entire Supabase stack locally or in production using Docker Compose. Clone the official repo and start all services.

    # Clone the Supabase repository
    git clone --depth 1 https://github.com/supabase/supabase
    
    cd supabase/docker
    
    # Copy example env file and customize
    cp .env.example .env
    # Edit .env to set passwords, JWT secrets, etc.
    
    # Start all services
    docker compose up -d
    
    # Services will be available at:
    # - Studio: http://localhost:3000
    # - API: http://localhost:8000
    # - Database: postgresql://postgres:your-password@localhost:5432/postgres
    ⚠ Heads up: Self-hosting requires managing secrets, backups, scaling, and security updates. Supabase Cloud handles all of this automatically.
  23. Step 23

    Use Edge Functions for server-side logic

    Supabase Edge Functions (powered by Deno Deploy) let you run custom server-side code globally. Write TypeScript functions that run on every request.

    # Create a new edge function
    supabase functions new my-function
    
    # Serve functions locally for development
    supabase functions serve
    
    # Deploy function to cloud project
    supabase functions deploy my-function
  24. Step 24

    Write an Edge Function

    Edge Functions use Deno runtime with access to the Supabase client. Great for webhooks, scheduled jobs, or custom API endpoints.

    // supabase/functions/my-function/index.ts
    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) => {
      const supabaseClient = createClient(
        Deno.env.get('SUPABASE_URL') ?? '',
        Deno.env.get('SUPABASE_ANON_KEY') ?? '',
        {
          global: {
            headers: { Authorization: req.headers.get('Authorization')! },
          },
        }
      )
    
      // Your custom logic here
      const { data: todos } = await supabaseClient
        .from('todos')
        .select('*')
    
      return new Response(JSON.stringify({ todos }), {
        headers: { 'Content-Type': 'application/json' },
      })
    })
  25. Step 25

    Invoke Edge Functions from the client

    Call your deployed Edge Functions from the client using .functions.invoke(). Pass headers, body, and query parameters.

    // Invoke the function
    const { data, error } = await supabase.functions.invoke('my-function', {
      body: { message: 'Hello from client' },
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    if (!error) {
      console.log('Function response:', data)
    }
  26. Step 26

    Monitor with built-in observability

    Supabase Cloud includes logs, metrics, and query performance insights. Access them via the dashboard under Logs, Database > Query Performance, and API > Logs.

    # View logs in real-time (CLI)
    supabase logs --project-ref <your-project-ref>
    
    # Filter by service
    supabase logs --project-ref <your-project-ref> --service postgres
    
    # Or access via dashboard:
    # Dashboard > Logs > Filter by service/level/timestamp
  27. Step 27

    Use GraphQL (pg_graphql)

    Supabase includes native GraphQL support via the pg_graphql extension. Query your database using GraphQL instead of REST if you prefer.

    # GraphQL endpoint: https://<project-ref>.supabase.co/graphql/v1
    
    query GetTodos {
      todosCollection {
        edges {
          node {
            id
            title
            is_complete
            created_at
          }
        }
      }
    }
    
    mutation InsertTodo($title: String!, $user_id: UUID!) {
      insertIntotodosCollection(
        objects: [{ title: $title, user_id: $user_id }]
      ) {
        records {
          id
          title
        }
      }
    }
  28. Step 28

    Integrate with Next.js

    Supabase works seamlessly with Next.js. Use Server Components for data fetching, Server Actions for mutations, and client components for interactivity.

    // app/lib/supabase/server.ts (Server Components)
    import { createServerClient } 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) {
              cookiesToSet.forEach(({ name, value, options }) =>
                cookieStore.set(name, value, options)
              )
            },
          },
        }
      )
    }
    
    // app/todos/page.tsx (Server Component)
    import { createClient } from '@/lib/supabase/server'
    
    export default async function TodosPage() {
      const supabase = await createClient()
      const { data: todos } = await supabase
        .from('todos')
        .select('*')
    
      return (
        <ul>
          {todos?.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )
    }
  29. Step 29

    Database backups and point-in-time recovery

    Supabase Cloud automatically backs up your database daily. Pro and higher plans support Point-In-Time Recovery (PITR) for restoring to any second within the retention window.

    # Create an on-demand backup (via dashboard)
    # Dashboard > Database > Backups > Create Backup
    
    # Download a backup (CLI)
    supabase db dump --project-ref <your-project-ref> > backup.sql
    
    # Restore from backup
    psql -h db.<your-project-ref>.supabase.co -U postgres -d postgres -f backup.sql
    ⚠ Heads up: Always test restores. PITR is available on Pro plans and above; Free tier has daily backups with 7-day retention.

Feature requests

Sign in to suggest features or vote on existing ones.

No feature requests yet.

Discussion

0 people marked this as worked·Sign in to mark your own.

Sign in to join the discussion.

No comments yet.