Supabase - Kompletny przewodnik po open-source alternatywie dla Firebase
Czym jest Supabase i dlaczego jest tak popularne?
Supabase to open-source Backend-as-a-Service (BaaS), który jest często nazywany "open-source Firebase". Ale w przeciwieństwie do Firebase, który używa NoSQL (Firestore), Supabase jest zbudowany na PostgreSQL - najpotężniejszej open-source relacyjnej bazie danych.
Supabase oferuje wszystko, czego potrzebujesz do zbudowania nowoczesnej aplikacji:
- Baza danych PostgreSQL z potężnym query builder
- Autentykacja z social logins i magic links
- Storage dla plików i obrazów
- Real-time subscriptions
- Edge Functions (Deno)
- Vector embeddings dla AI
Dlaczego wybrać Supabase?
Open Source
Cały kod Supabase jest dostępny na GitHub. Możesz go self-hostować, modyfikować i masz pewność, że nie zostaniesz uzależniony od jednego dostawcy.
PostgreSQL pod spodem
Dostajesz pełną moc PostgreSQL:
- ACID transactions
- Foreign keys i constraints
- Full-text search
- JSON/JSONB support
- Extensions (PostGIS, pg_vector, etc.)
- Row Level Security
Developer Experience
Świetne SDK, automatycznie generowana dokumentacja API, dashboard do zarządzania danymi i łatwa integracja z popularnymi frameworkami.
Instalacja i konfiguracja
Utworzenie projektu
- Załóż konto na supabase.com
- Stwórz nowy projekt
- Zapisz URL i klucze API
Instalacja SDK
# JavaScript/TypeScript
npm install @supabase/supabase-js
# React specyficzne hooki
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjsKonfiguracja klienta
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
// Typy generowane przez Supabase CLI
import { Database } from '@/types/database.types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Server-side client (Next.js App Router)
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
export const createServerClient = () => {
return createServerComponentClient<Database>({ cookies })
}Zmienne środowiskowe
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # Tylko server-side!Baza danych PostgreSQL
Tworzenie tabel
W Supabase Dashboard lub przez SQL:
-- Tabela użytkowników (rozszerza auth.users)
CREATE TABLE public.profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tabela postów
CREATE TABLE public.posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
author_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT,
slug TEXT UNIQUE NOT NULL,
published BOOLEAN DEFAULT FALSE,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indeksy dla wydajności
CREATE INDEX posts_author_id_idx ON public.posts(author_id);
CREATE INDEX posts_published_idx ON public.posts(published) WHERE published = true;
CREATE INDEX posts_slug_idx ON public.posts(slug);CRUD Operations
// SELECT - pobieranie danych
const { data: posts, error } = await supabase
.from('posts')
.select(`
id,
title,
slug,
content,
published_at,
author:profiles(id, username, avatar_url)
`)
.eq('published', true)
.order('published_at', { ascending: false })
.limit(10)
// SELECT z filtrowaniem
const { data } = await supabase
.from('posts')
.select('*')
.or('title.ilike.%react%,content.ilike.%react%')
.gte('published_at', '2024-01-01')
.lte('published_at', '2024-12-31')
// INSERT
const { data: newPost, error } = await supabase
.from('posts')
.insert({
author_id: userId,
title: 'My First Post',
slug: 'my-first-post',
content: 'Hello World!',
})
.select()
.single()
// UPDATE
const { data, error } = await supabase
.from('posts')
.update({
title: 'Updated Title',
updated_at: new Date().toISOString(),
})
.eq('id', postId)
.select()
.single()
// DELETE
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// UPSERT (insert or update)
const { data, error } = await supabase
.from('profiles')
.upsert({
id: userId,
username: 'john_doe',
full_name: 'John Doe',
})
.select()
.single()Zaawansowane zapytania
// Paginacja
const pageSize = 10
const page = 1
const { data, count } = await supabase
.from('posts')
.select('*', { count: 'exact' })
.range((page - 1) * pageSize, page * pageSize - 1)
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'react hooks', {
type: 'websearch',
config: 'english'
})
// Agregacje przez RPC
// Najpierw stwórz funkcję w SQL:
// CREATE FUNCTION get_post_stats() RETURNS TABLE (total bigint, published bigint)
// AS $$ SELECT COUNT(*), COUNT(*) FILTER (WHERE published) FROM posts $$
// LANGUAGE sql;
const { data } = await supabase.rpc('get_post_stats')Autentykacja
Email/Password
// Rejestracja
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword123',
options: {
data: {
full_name: 'John Doe',
},
emailRedirectTo: 'https://myapp.com/auth/callback',
},
})
// Logowanie
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword123',
})
// Wylogowanie
await supabase.auth.signOut()
// Reset hasła
await supabase.auth.resetPasswordForEmail('user@example.com', {
redirectTo: 'https://myapp.com/reset-password',
})OAuth (Social Logins)
// GitHub
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://myapp.com/auth/callback',
scopes: 'read:user user:email',
},
})
// Google
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://myapp.com/auth/callback',
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
// Obsługiwane providery:
// google, github, gitlab, bitbucket, azure, discord,
// facebook, twitter, apple, spotify, slack, twitch, notionMagic Link (Passwordless)
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://myapp.com/auth/callback',
},
})Sesja użytkownika
// Pobierz aktualną sesję
const { data: { session } } = await supabase.auth.getSession()
// Pobierz użytkownika
const { data: { user } } = await supabase.auth.getUser()
// Nasłuchuj zmian autentykacji
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event)
console.log('Session:', session)
if (event === 'SIGNED_IN') {
// Użytkownik się zalogował
} else if (event === 'SIGNED_OUT') {
// Użytkownik się wylogował
} else if (event === 'TOKEN_REFRESHED') {
// Token został odświeżony
}
})Next.js App Router Integration
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = createRouteHandlerClient({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(new URL('/', request.url))
}// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const { data: { session } } = await supabase.auth.getSession()
// Chroń trasy wymagające logowania
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}Row Level Security (RLS)
RLS to kluczowa funkcja bezpieczeństwa Supabase. Pozwala definiować polityki dostępu na poziomie wierszy.
-- Włącz RLS na tabeli
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
-- Polityka: każdy może czytać opublikowane posty
CREATE POLICY "Public posts are viewable by everyone"
ON public.posts FOR SELECT
USING (published = true);
-- Polityka: użytkownicy mogą czytać swoje własne posty
CREATE POLICY "Users can view own posts"
ON public.posts FOR SELECT
USING (auth.uid() = author_id);
-- Polityka: użytkownicy mogą tworzyć posty
CREATE POLICY "Users can create posts"
ON public.posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Polityka: użytkownicy mogą edytować swoje posty
CREATE POLICY "Users can update own posts"
ON public.posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Polityka: użytkownicy mogą usuwać swoje posty
CREATE POLICY "Users can delete own posts"
ON public.posts FOR DELETE
USING (auth.uid() = author_id);
-- Polityka z rolami (np. admin)
CREATE POLICY "Admins can do everything"
ON public.posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM public.profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
);Storage
Upload plików
// Upload z przeglądarki
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${userId}/${Date.now()}.${fileExt}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file, {
cacheControl: '3600',
upsert: true,
})
// Pobierz publiczny URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName)Pobieranie i usuwanie
// Download
const { data, error } = await supabase.storage
.from('documents')
.download('folder/file.pdf')
// Lista plików
const { data: files } = await supabase.storage
.from('documents')
.list('folder', {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' },
})
// Usuwanie
const { error } = await supabase.storage
.from('avatars')
.remove(['avatar1.png', 'avatar2.png'])Polityki Storage
-- Polityka: użytkownicy mogą uploadować do swojego folderu
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- Polityka: publiczny dostęp do odczytu
CREATE POLICY "Avatars are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');Real-time Subscriptions
// Nasłuchuj zmian w tabeli
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE lub *
schema: 'public',
table: 'posts',
filter: 'published=eq.true', // Opcjonalny filtr
},
(payload) => {
console.log('Change received!', payload)
if (payload.eventType === 'INSERT') {
// Nowy post
setPosts(prev => [payload.new, ...prev])
} else if (payload.eventType === 'UPDATE') {
// Zaktualizowany post
setPosts(prev =>
prev.map(p => p.id === payload.new.id ? payload.new : p)
)
} else if (payload.eventType === 'DELETE') {
// Usunięty post
setPosts(prev => prev.filter(p => p.id !== payload.old.id))
}
}
)
.subscribe()
// Cleanup
return () => {
supabase.removeChannel(channel)
}Broadcast (Custom Events)
// Wysyłanie
const channel = supabase.channel('room:123')
channel.send({
type: 'broadcast',
event: 'cursor-position',
payload: { x: 100, y: 200 },
})
// Odbieranie
channel.on('broadcast', { event: 'cursor-position' }, (payload) => {
console.log('Cursor at:', payload.payload)
})Presence (Online Status)
const channel = supabase.channel('room:123')
// Track presence
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', Object.keys(state).length)
})
channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key)
})
channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key)
})
// Track current user
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString(),
})
}
})Edge Functions
Edge Functions to serverless functions napisane w TypeScript (Deno):
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { to, subject, body } = await req.json()
// Wyślij email używając np. Resend
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@myapp.com',
to,
subject,
html: body,
}),
})
const data = await response.json()
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } }
)
})Wywołanie z klienta:
const { data, error } = await supabase.functions.invoke('send-email', {
body: {
to: 'user@example.com',
subject: 'Welcome!',
body: '<h1>Welcome to our app</h1>',
},
})Generowanie typów TypeScript
# Zainstaluj Supabase CLI
npm install -g supabase
# Zaloguj się
supabase login
# Wygeneruj typy
supabase gen types typescript --project-id your-project-id > types/database.types.tsUżycie wygenerowanych typów:
import { Database } from '@/types/database.types'
type Post = Database['public']['Tables']['posts']['Row']
type NewPost = Database['public']['Tables']['posts']['Insert']
type UpdatePost = Database['public']['Tables']['posts']['Update']
// Teraz masz pełne typowanie
const { data } = await supabase
.from('posts')
.select('*')
.single()
// data jest typu Post | nullPorównanie Supabase vs Firebase
| Cecha | Supabase | Firebase |
|---|---|---|
| Baza danych | PostgreSQL (relacyjna) | Firestore (NoSQL) |
| Open Source | Tak | Nie |
| Self-hosting | Tak | Nie |
| SQL | Pełne wsparcie | Brak |
| Relacje | Foreign keys, JOINs | Brak |
| Real-time | PostgreSQL changes | Firestore snapshots |
| Auth | Podobne możliwości | Podobne możliwości |
| Storage | Podobne możliwości | Podobne możliwości |
| Edge Functions | Deno | Node.js |
| Pricing | Bardziej przejrzysty | Pay-as-you-go |
Cennik (2025)
Free
- 500 MB bazy danych
- 1 GB storage
- 2 GB bandwidth
- 50,000 MAU (auth)
- 500,000 Edge Function invocations
Pro ($25/miesiąc)
- 8 GB bazy danych
- 100 GB storage
- 250 GB bandwidth
- 100,000 MAU
- 2M Edge Function invocations
- Daily backups
Team ($599/miesiąc)
- Wszystko z Pro
- SOC2 compliance
- SSO/SAML
- Priority support
- 14-day PITR
Enterprise (Custom)
- Dedicated infrastructure
- Custom contracts
- SLA guarantees
Najlepsze praktyki
1. Zawsze używaj RLS
-- Nigdy nie zostawiaj tabel bez RLS w produkcji!
ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;2. Używaj typów TypeScript
# Regeneruj typy po każdej zmianie schematu
supabase gen types typescript --project-id xxx > types/database.types.ts3. Optymalizuj zapytania
// Wybieraj tylko potrzebne kolumny
const { data } = await supabase
.from('posts')
.select('id, title, slug') // Nie select('*')
.limit(10)4. Używaj indeksów
-- Dodawaj indeksy dla często filtrowanych kolumn
CREATE INDEX posts_published_idx ON posts(published_at)
WHERE published = true;Często zadawane pytania (FAQ)
Czy Supabase jest darmowe?
Tak, plan Free pozwala na budowanie i testowanie aplikacji. Dla produkcji zalecany jest plan Pro.
Czy mogę self-hostować Supabase?
Tak, wszystkie komponenty są open-source. Dokumentacja zawiera instrukcje dla Docker i Kubernetes.
Jak migrować z Firebase?
Supabase oferuje narzędzia do migracji. Główna zmiana to przejście z NoSQL na SQL model danych.
Czy Supabase skaluje się?
Tak, PostgreSQL jest znany z dobrej skalowalności. Supabase oferuje też read replicas i connection pooling.
Podsumowanie
Supabase to potężna platforma, która łączy najlepsze cechy tradycyjnych baz danych (PostgreSQL) z nowoczesnym developer experience. Open-source nature, przejrzysty pricing i bogaty zestaw funkcji sprawiają, że jest świetnym wyborem dla startupów i zespołów produktowych.
Kluczowe zalety:
- PostgreSQL z pełną mocą SQL
- Row Level Security dla bezpieczeństwa
- Real-time subscriptions
- Bogaty system autentykacji
- Self-hosting możliwy
- Aktywna społeczność