Supabase: Open-Source Firebase Alternative
Backend-as-a-Service for your homelab. Postgres, Auth, Storage, and Realtime.
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
Method 1: Docker Compose (Recommended)
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):
- Navigate to Studio at
http://localhost:3001 - Go to “Table Editor” in the sidebar
- Click “New Table”
- 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
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
Configure in Supabase:
- Studio > Authentication > Providers > GitHub
- Enable GitHub provider
- Enter Client ID and Client Secret
- Save
Use in your app:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://yourapp.com/auth/callback'
}
}) Magic Links
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:
- Port conflicts - Change ports in docker-compose.yml
- Memory issues - Increase Docker memory limit
- 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:
- Explore the Supabase documentation
- Join the Supabase Discord community
- Build your first application
- Set up monitoring and backups
- Explore advanced PostgreSQL features
Start building with Supabase and enjoy the power of Firebase with the freedom of self-hosting!