Development intermediate ⏱ 60 minutes

Supabase: Open-Source Firebase Alternative

Backend-as-a-Service for your homelab. Postgres, Auth, Storage, and Realtime.

By Homelab Team • 2025-01-15
#supabase#baas#postgres#auth#docker

Introduction

Supabase is an open-source Firebase alternative that provides everything you need to build modern applications: a PostgreSQL database, authentication, instant APIs, real-time subscriptions, storage, and edge functions. Unlike Firebase, Supabase gives you complete control over your data and infrastructure by being fully self-hostable.

Why Supabase for your homelab?

  • PostgreSQL-powered - Battle-tested relational database with excellent performance
  • Instant REST API - Auto-generated API from your database schema
  • Real-time subscriptions - WebSocket support for live data updates
  • Built-in authentication - Email, OAuth, magic links, and more
  • File storage - S3-compatible object storage
  • Edge functions - Serverless TypeScript/JavaScript functions
  • Open source - Full control, no vendor lock-in

Supabase is perfect for building internal tools, dashboards, mobile apps, and web applications that need a robust backend without the complexity of building everything from scratch.

Prerequisites

Before getting started, ensure you have:

  • Docker and Docker Compose - Version 20.10+ and Compose V2
  • System resources - Minimum 4GB RAM, 8GB+ recommended
  • Domain name - For production deployments with SSL
  • Basic SQL knowledge - Understanding of relational databases
  • Git - For cloning the Supabase repository

Nice to have:

  • Experience with PostgreSQL
  • Understanding of REST APIs
  • Familiarity with JWT authentication
  • Knowledge of reverse proxies (nginx/Traefik)

Understanding Supabase Architecture

Supabase consists of multiple services that work together:

Core Services

Kong API Gateway

  • Routes requests to appropriate services
  • Handles rate limiting and CORS
  • Manages API keys and authentication
  • Port: 8000 (HTTP), 8443 (HTTPS)

PostgreSQL Database

  • Core data storage with full SQL support
  • Includes extensions: PostGIS, pg_cron, timescaledb
  • Built-in row-level security (RLS)
  • Port: 5432

PostgREST

  • Auto-generates REST API from database schema
  • Respects row-level security policies
  • Supports filtering, ordering, pagination
  • Port: 3000 (internal)

GoTrue

  • Authentication service
  • Supports email/password, OAuth, magic links
  • JWT token generation and validation
  • User management APIs
  • Port: 9999 (internal)

Realtime

  • WebSocket server for live data
  • Broadcasts database changes
  • Presence tracking
  • Built on Elixir/Phoenix
  • Port: 4000 (internal)

Storage

  • S3-compatible object storage
  • Image transformation and optimization
  • Access control and signed URLs
  • Port: 5000 (internal)

Meta

  • Database management API
  • Schema migrations
  • Backup management
  • Port: 8080 (internal)

Studio

  • Web-based database management UI
  • Table editor, SQL editor, API docs
  • User management interface
  • Port: 3001

All these services communicate through Kong, which provides a unified API endpoint.

Installation and Setup

This is the official method for self-hosting Supabase.

Step 1: Clone the Supabase repository

# Create project directory
mkdir -p ~/supabase-homelab
cd ~/supabase-homelab

# Clone Supabase with Docker setup
git clone --depth 1 https://github.com/supabase/supabase.git
cd supabase/docker

Step 2: Copy and configure environment variables

# Copy example environment file
cp .env.example .env

# Generate secure secrets
openssl rand -base64 32  # Use for POSTGRES_PASSWORD
openssl rand -base64 32  # Use for JWT_SECRET
openssl rand -base64 32  # Use for ANON_KEY
openssl rand -base64 32  # Use for SERVICE_ROLE_KEY

Step 3: Edit .env file with critical settings

nano .env

Key variables to configure:

############
# Secrets
############
POSTGRES_PASSWORD=your-super-secret-password
JWT_SECRET=your-jwt-secret-at-least-32-characters-long
ANON_KEY=your-anon-key-generated-above
SERVICE_ROLE_KEY=your-service-role-key-generated-above

############
# Database
############
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432

############
# API Proxy
############
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443

############
# Studio
############
STUDIO_PORT=3001
PUBLIC_REST_URL=http://localhost:8000/rest/v1/

############
# Auth
############
SITE_URL=http://localhost:3001
ADDITIONAL_REDIRECT_URLS=
JWT_EXPIRY=3600
DISABLE_SIGNUP=false

# Email (configure SMTP for production)
[email protected]
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASS=your-app-password
SMTP_SENDER_NAME=Supabase Homelab

############
# Dashboard
############
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=your-dashboard-password

############
# Storage
############
STORAGE_BACKEND=file
FILE_SIZE_LIMIT=52428800

Step 4: Launch Supabase

# Start all services
docker compose up -d

# Monitor startup logs
docker compose logs -f

# Verify all services are healthy
docker compose ps

Step 5: Access Supabase Studio

Navigate to http://localhost:3001 and log in with your dashboard credentials.

Method 2: Production Deployment with Reverse Proxy

For production use, you’ll want SSL/TLS and a domain name.

nginx reverse proxy configuration:

# Studio (Web UI)
server {
    listen 443 ssl http2;
    server_name studio.supabase.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/studio.supabase.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/studio.supabase.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# API Gateway (Kong)
server {
    listen 443 ssl http2;
    server_name api.supabase.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/api.supabase.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.supabase.yourdomain.com/privkey.pem;

    client_max_body_size 50M;

    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support for Realtime
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Update your .env file:

SITE_URL=https://studio.supabase.yourdomain.com
PUBLIC_REST_URL=https://api.supabase.yourdomain.com/rest/v1/

Restart services: docker compose restart

Database Configuration

Creating Your First Database

Using Studio (Web UI):

  1. Navigate to Studio at http://localhost:3001
  2. Go to “Table Editor” in the sidebar
  3. Click “New Table”
  4. Create a sample table:
-- Create users table
CREATE TABLE public.users (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    full_name TEXT,
    avatar_url TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
);

-- Create posts table
CREATE TABLE public.posts (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
    title TEXT NOT NULL,
    content TEXT,
    published BOOLEAN DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
);

-- Create index for better query performance
CREATE INDEX posts_user_id_idx ON public.posts(user_id);
CREATE INDEX posts_created_at_idx ON public.posts(created_at DESC);

Row Level Security (RLS)

RLS is Supabase’s powerful security feature that controls data access at the row level.

Enable RLS:

-- Enable RLS on tables
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;

Create security policies:

-- Users can read their own user record
CREATE POLICY "Users can view their own data"
ON public.users FOR SELECT
USING (auth.uid() = id);

-- Users can update their own user record
CREATE POLICY "Users can update their own data"
ON public.users FOR UPDATE
USING (auth.uid() = id);

-- Anyone can read published posts
CREATE POLICY "Published posts are public"
ON public.posts FOR SELECT
USING (published = true);

-- Users can create their own posts
CREATE POLICY "Users can create their own posts"
ON public.posts FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can update their own posts
CREATE POLICY "Users can update their own posts"
ON public.posts FOR UPDATE
USING (auth.uid() = user_id);

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

Database Functions

Create reusable database functions:

-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = TIMEZONE('utc', NOW());
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Trigger to automatically update updated_at
CREATE TRIGGER update_users_updated_at
    BEFORE UPDATE ON public.users
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_posts_updated_at
    BEFORE UPDATE ON public.posts
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

Authentication Setup

Email/Password Authentication

Authentication is automatically enabled. Here’s how to use it:

Sign up a user (JavaScript/TypeScript):

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

const supabaseUrl = 'http://localhost:8000'
const supabaseAnonKey = 'your-anon-key-from-env'

const supabase = createClient(supabaseUrl, supabaseAnonKey)

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password-here',
  options: {
    data: {
      full_name: 'John Doe',
      avatar_url: 'https://example.com/avatar.jpg'
    }
  }
})

Sign in:

const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password-here'
})

// Access token is automatically stored
console.log(data.session.access_token)

Get current user:

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

Sign out:

await supabase.auth.signOut()

OAuth Providers

Configure OAuth providers in Studio under Authentication > Providers.

Example: GitHub OAuth

  1. Create GitHub OAuth App:

    • Go to GitHub Settings > Developer settings > OAuth Apps
    • New OAuth App
    • Homepage URL: https://studio.supabase.yourdomain.com
    • Callback URL: https://api.supabase.yourdomain.com/auth/v1/callback
  2. Configure in Supabase:

    • Studio > Authentication > Providers > GitHub
    • Enable GitHub provider
    • Enter Client ID and Client Secret
    • Save
  3. Use in your app:

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

Passwordless authentication via email:

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

API Usage and Examples

REST API

Supabase auto-generates a RESTful API from your database schema.

Fetching data:

// Get all published posts
const { data, error } = await supabase
  .from('posts')
  .select('*')
  .eq('published', true)

// Get posts with user data (JOIN)
const { data, error } = await supabase
  .from('posts')
  .select(`
    *,
    users (
      full_name,
      avatar_url
    )
  `)
  .eq('published', true)

// Filtering and pagination
const { data, error } = await supabase
  .from('posts')
  .select('*')
  .gte('created_at', '2025-01-01')
  .order('created_at', { ascending: false })
  .range(0, 9)  // First 10 results

Inserting data:

const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'My First Post',
    content: 'This is the content of my post',
    user_id: user.id,
    published: true
  })
  .select()

Updating data:

const { data, error } = await supabase
  .from('posts')
  .update({ published: true })
  .eq('id', postId)
  .select()

Deleting data:

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

Real-time Subscriptions

Listen to database changes in real-time:

// Subscribe to all INSERT events on posts table
const channel = supabase
  .channel('posts-channel')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('New post created:', payload.new)
    }
  )
  .subscribe()

// Subscribe to all changes on a specific post
const postChannel = supabase
  .channel('post-123')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts',
      filter: 'id=eq.123'
    },
    (payload) => {
      console.log('Post changed:', payload)
    }
  )
  .subscribe()

// Unsubscribe when done
supabase.removeChannel(channel)

Storage API

Upload and manage files:

// Upload file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .upload('public/avatar1.png', file, {
    cacheControl: '3600',
    upsert: false
  })

// Get public URL
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('public/avatar1.png')

// Download file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .download('public/avatar1.png')

// Delete file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .remove(['public/avatar1.png'])

// List files
const { data, error } = await supabase
  .storage
  .from('avatars')
  .list('public', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

Create storage bucket with policies:

-- Create bucket via SQL
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);

-- Allow authenticated users to upload
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]
);

-- Allow public read access
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

Edge Functions

Deploy serverless TypeScript/JavaScript functions.

Create a function:

# Install Supabase CLI
npm install -g supabase

# Initialize project
supabase init

# Create new function
supabase functions new hello-world

Edit function code (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 {
    // Get auth header
    const authHeader = req.headers.get('Authorization')!

    // Create Supabase client
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      { global: { headers: { Authorization: authHeader } } }
    )

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

    // Query database
    const { data, error } = await supabase
      .from('posts')
      .select('*')
      .eq('user_id', user?.id)

    if (error) throw error

    return new Response(
      JSON.stringify({ posts: data, user }),
      { headers: { "Content-Type": "application/json" } },
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 400, headers: { "Content-Type": "application/json" } },
    )
  }
})

Deploy function:

supabase functions deploy hello-world

Call function from client:

const { data, error } = await supabase.functions.invoke('hello-world', {
  body: { name: 'World' }
})

Example Integrations

Next.js Integration

npm install @supabase/supabase-js

Create Supabase client (lib/supabase.ts):

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

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

Use in pages (pages/index.tsx):

import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'

export default function Home() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetchPosts()

    // Subscribe to changes
    const channel = supabase
      .channel('posts')
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'posts' },
        () => fetchPosts()
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [])

  async function fetchPosts() {
    const { data } = await supabase
      .from('posts')
      .select('*')
      .order('created_at', { ascending: false })

    setPosts(data || [])
  }

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  )
}

Python Integration

pip install supabase
from supabase import create_client, Client

url: str = "http://localhost:8000"
key: str = "your-anon-key"
supabase: Client = create_client(url, key)

# Fetch data
response = supabase.table('posts').select("*").eq('published', True).execute()
posts = response.data

# Insert data
data = supabase.table('posts').insert({
    "title": "New Post",
    "content": "Post content",
    "user_id": user_id,
    "published": True
}).execute()

# Authenticate
auth_response = supabase.auth.sign_in_with_password({
    "email": "[email protected]",
    "password": "password"
})

Backup Procedures

Database Backups

Manual backup:

# Backup database
docker exec supabase-db pg_dump -U postgres postgres > supabase_backup_$(date +%Y%m%d_%H%M%S).sql

# Restore database
cat supabase_backup_20250115.sql | docker exec -i supabase-db psql -U postgres postgres

Automated backup script:

#!/bin/bash
# backup-supabase.sh

BACKUP_DIR="/backups/supabase"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup database
docker exec supabase-db pg_dump -U postgres postgres |     gzip > "$BACKUP_DIR/db_$DATE.sql.gz"

# Backup storage files
docker run --rm     -v supabase_storage:/data     -v $BACKUP_DIR:/backup     alpine tar czf /backup/storage_$DATE.tar.gz /data

# Remove old backups
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "storage_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup completed: $DATE"

Schedule with cron:

# Run daily at 2 AM
0 2 * * * /home/user/backup-supabase.sh >> /var/log/supabase-backup.log 2>&1

Configuration Backups

# Backup environment and docker compose files
tar czf supabase-config-$(date +%Y%m%d).tar.gz     .env     docker-compose.yml     volumes/

Troubleshooting

Service Won’t Start

Check service status:

docker compose ps
docker compose logs service-name

Common issues:

  1. Port conflicts - Change ports in docker-compose.yml
  2. Memory issues - Increase Docker memory limit
  3. Permission errors - Check volume permissions

Database Connection Errors

Verify PostgreSQL is running:

docker compose exec db psql -U postgres -c "SELECT version();"

Check connection from Kong:

docker compose exec kong curl http://db:5432

Authentication Issues

JWT token problems:

  • Verify JWT_SECRET matches across all services
  • Check token expiry time
  • Ensure clock sync on host machine

Reset user password:

-- Connect to database
docker compose exec db psql -U postgres

-- Update user password (hashed with bcrypt)
UPDATE auth.users
SET encrypted_password = crypt('newpassword', gen_salt('bf'))
WHERE email = '[email protected]';

API Returns 403 Forbidden

Check RLS policies:

-- View policies for a table
SELECT * FROM pg_policies WHERE tablename = 'posts';

-- Temporarily disable RLS for testing (NOT for production)
ALTER TABLE public.posts DISABLE ROW LEVEL SECURITY;

Storage Upload Fails

Check storage bucket policies:

SELECT * FROM storage.buckets;
SELECT * FROM storage.objects LIMIT 10;

Verify file size limits in .env:

FILE_SIZE_LIMIT=52428800  # 50MB in bytes

Performance Issues

Check database stats:

-- Show slow queries
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Show table sizes
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;

Optimize queries with indexes:

-- Add index for common queries
CREATE INDEX posts_published_idx ON posts(published) WHERE published = true;
CREATE INDEX posts_user_created_idx ON posts(user_id, created_at DESC);

Production Best Practices

Security Checklist

  • Change all default passwords
  • Use strong JWT_SECRET (32+ characters)
  • Enable HTTPS with valid certificates
  • Configure firewall rules
  • Enable RLS on all tables
  • Limit SERVICE_ROLE_KEY usage
  • Configure SMTP for email verification
  • Set up rate limiting in Kong
  • Regular security updates
  • Monitor authentication logs

Performance Optimization

PostgreSQL tuning:

-- Increase shared buffers (25% of RAM)
ALTER SYSTEM SET shared_buffers = '2GB';

-- Increase work memory
ALTER SYSTEM SET work_mem = '64MB';

-- Enable query planning
ALTER SYSTEM SET random_page_cost = 1.1;

-- Reload configuration
SELECT pg_reload_conf();

Connection pooling is included via PgBouncer in the Docker setup.

Monitoring

Monitor with Grafana/Prometheus:

Add monitoring to docker-compose.yml:

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "3002:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Key metrics to monitor:

  • Database connection count
  • API response times
  • Authentication success/failure rate
  • Storage usage
  • Memory and CPU usage
  • Error rates

Conclusion

Supabase provides a complete backend-as-a-service solution that you can fully control in your homelab. With PostgreSQL at its core, instant APIs, authentication, real-time capabilities, and storage, you have everything needed to build production-ready applications.

Key takeaways:

  • Supabase is production-ready out of the box
  • Row Level Security provides fine-grained access control
  • Real-time subscriptions enable live updates
  • Edge functions add serverless compute
  • Complete control over your data and infrastructure

Next steps:

Start building with Supabase and enjoy the power of Firebase with the freedom of self-hosting!