Panduan Lengkap Supabase untuk Developer Indonesia
Kamis, 25 Des 2025
Kalau kamu pernah pakai Firebase dan merasa “kok kayaknya terlalu vendor-locked ya?”, selamat datang di klub. Supabase hadir sebagai alternatif open-source yang powerful, dan yang paling penting: built di atas PostgreSQL—database yang sudah battle-tested selama puluhan tahun.
Di artikel ini, gue bakal bahas tuntas semua yang perlu kamu tahu tentang Supabase. Dari setup sampai production-ready. Let’s go!
Apa Itu Supabase?
Supabase adalah open-source Backend-as-a-Service (BaaS) yang menyediakan:
- PostgreSQL Database - Full Postgres dengan GUI yang ciamik
- Authentication - Email, OAuth, Magic Link, Phone Auth
- Storage - Object storage untuk files dan media
- Edge Functions - Serverless functions pakai Deno
- Realtime - Subscribe ke database changes secara live
- Vector/AI - Untuk aplikasi AI dengan pgvector
Yang bikin Supabase menarik adalah kamu bisa self-host kalau mau, atau pakai managed service mereka. Data kamu, rules kamu.
Setup Project Supabase
1. Buat Akun dan Project
Pertama, daftar di supabase.com dan buat project baru:
- Klik “New Project”
- Pilih organization (atau buat baru)
- Isi nama project dan database password
- Pilih region (Singapore paling deket untuk Indonesia)
- Tunggu beberapa menit sampai provisioning selesai
2. Ambil API Keys
Setelah project ready, ambil credentials di Settings > API:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxxxxx
SUPABASE_SERVICE_ROLE_KEY=eyJxxxxxx # Jangan expose ke client!
Penting: ANON_KEY aman di-expose ke browser karena dibatasi oleh Row Level Security. SERVICE_ROLE_KEY bypass semua security, jadi simpan baik-baik di server only.
Database: Tables, Relationships, dan RLS
Membuat Tables
Kamu bisa buat table via GUI di dashboard atau pakai SQL Editor:
-- Buat table profiles
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()
);
-- Buat table posts
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 untuk performa
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = TRUE;
Relationships
Supabase auto-detect foreign keys dan bikin relationships. Query dengan joins jadi gampang:
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 adalah game-changer. Kamu define security rules di level database, bukan di application code:
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Semua orang bisa baca posts yang published
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = TRUE);
-- Policy: User hanya bisa CRUD posts miliknya
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 bisa baca posts sendiri (termasuk draft)
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
Tips: Selalu test RLS policies pakai “SQL Editor” dengan role anon atau authenticated.
Authentication
Supabase Auth support berbagai metode. Semua udah built-in, tinggal 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 di dashboard (Authentication > Providers), lalu:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
},
});
Magic Link
Perfect untuk 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 ke dashboard
} else if (event === 'SIGNED_OUT') {
// Redirect ke login
}
});
// Get current user
const { data: { user } } = await supabase.auth.getUser();
Storage: Upload dan Manage Files
Supabase Storage mirip S3 tapi lebih simple. Cocok untuk avatars, images, documents, dll.
Setup Bucket
Buat bucket di dashboard atau via SQL:
-- Buat bucket public untuk avatars
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', TRUE);
-- Buat bucket private untuk documents
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', FALSE);
Storage Policies
-- Policy: User bisa upload ke folder mereka sendiri
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 bisa dilihat semua orang
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 (untuk private buckets)
const { data, error } = await supabase.storage
.from('documents')
.download('path/to/file.pdf');
Edge Functions
Edge Functions adalah serverless functions yang jalan di Deno. Perfect untuk:
- Webhooks
- Third-party API calls
- Complex business logic
- Scheduled jobs
Buat Function
# Install Supabase CLI
npm install -g supabase
# Init dan buat 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 dari 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 dan Invoke
# Deploy
supabase functions deploy hello-world
# Test locally
supabase functions serve hello-world
// Invoke dari client
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'Budi' },
});
Realtime Subscriptions
Fitur killer Supabase. Subscribe ke database changes secara real-time.
Subscribe ke Table Changes
// Subscribe ke semua changes di table posts
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE, atau *
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
// Hanya subscribe ke posts dari user tertentu
const channel = supabase
.channel('my-posts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`,
},
handleNewPost
)
.subscribe();
Broadcast dan Presence
Selain database changes, kamu juga bisa bikin 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() });
}
});
Integrasi dengan 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 untuk 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 dan Tips
1. Selalu Gunakan RLS
Jangan pernah disable RLS di production. Bahkan untuk admin operations, lebih baik pakai service role key di server daripada matiin RLS.
2. Optimize Queries
// ❌ Bad: Fetch semua columns
const { data } = await supabase.from('posts').select('*');
// ✅ Good: Fetch yang dibutuhkan aja
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 untuk Complex Logic
-- Buat function untuk 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
Untuk high-traffic apps, pakai pooler connection string (port 6543) bukan direct connection.
Pricing dan Free Tier
Supabase punya free tier yang cukup generous:
| Feature | Free Tier | Pro ($25/mo) |
|---|---|---|
| Database | 500MB | 8GB |
| Storage | 1GB | 100GB |
| Bandwidth | 2GB | 250GB |
| Edge Functions | 500K invocations | 2M invocations |
| Auth | 50K MAU | 100K MAU |
| Realtime | 200 concurrent | 500 concurrent |
Free tier cocok banget untuk:
- Side projects
- MVP dan prototyping
- Learning dan experimenting
Upgrade ke Pro kalau udah mulai scaling atau butuh fitur seperti:
- Daily backups
- Email support
- More resources
Kesimpulan
Supabase adalah pilihan solid untuk backend modern. Dengan PostgreSQL sebagai foundation, kamu dapat:
- Reliability - PostgreSQL udah proven di production selama puluhan tahun
- Flexibility - Full SQL access, bisa self-host, no vendor lock-in
- Developer Experience - Dashboard yang bagus, SDK yang intuitif
- Cost Effective - Free tier generous, pricing transparan
Kalau kamu coming from Firebase, transition ke Supabase relatively smooth. Konsepnya mirip, tapi dengan power of SQL dan open-source ecosystem.
Mulai explore di supabase.com dan join community Discord mereka yang aktif. Happy building! 🚀