Supabase to stack backendowy, który pozwala budować aplikacje produkcyjne bez zarządzania infrastrukturą. PostgreSQL pod spodem, REST i GraphQL API generowane automatycznie, auth out-of-the-box, storage dla plików, Realtime przez WebSockety. Wszystko open-source, możliwe do self-hostowania.
Porównanie z Firebase: Supabase to PostgreSQL z relacyjnym modelem danych — nie NoSQL. Możesz pisać JOIN, korzystasz z transakcji, masz pełny SQL. To znacząca różnica jeśli Twoje dane mają relacje (czyli prawie zawsze).
Ten artykuł zakłada znajomość TypeScript i podstaw SQL. Przejdziemy przez pełny setup — od `supabase init` do produkcyjnego checklist.
Local dev setup
Supabase CLI to twój przyjaciel przy lokalnym developmencie:
```bash npm install supabase --save-dev npx supabase init npx supabase start ```
`supabase start` odpala lokalne kontenery Docker: PostgreSQL, Auth, Storage, Studio (panel lokalny dostępny na localhost:54323). Dostajesz też lokalne URL i klucze anon/service_role w terminalu.
Zatrzymanie środowiska:
```bash npx supabase stop ```
Lokalny setup izoluje środowiska developerskie — każdy developer ma własną instancję bazy. Dobry nawyk: nie pisz bezpośrednio do zdalnej bazy w trakcie developmentu.
Tworzenie tabel: Dashboard, SQL editor, migracje
Masz trzy opcje:
**Dashboard** (supabase.com → Table Editor) — wizualny kreator tabel. Dobry dla prototypowania, ale nie nadaje się do pracy zespołowej — zmiany nie są śledzone w repozytorium.
**SQL Editor** — piszesz SQL bezpośrednio w panelu. Szybkie, ale tak samo nie wersjonowane.
**Migracje** — właściwe podejście dla produkcji:
```bash npx supabase migration new create_profiles_table ```
Tworzy plik w `supabase/migrations/`. Wypełnij go SQL:
```sql 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, created_at timestamptz default now() );
-- Włącz RLS od razu alter table public.profiles enable row level security; ```
Aplikuj lokalnie:
```bash npx supabase db reset ```
Wypchnij do zdalnej bazy:
```bash npx supabase db push ```
Migracje są w repozytorium — cały zespół ma ten sam schemat bazy. Code review dla zmian schematu jest możliwy.
TypeScript client z generowanymi typami
Generowanie typów na podstawie schematu bazy:
```bash npx supabase gen types typescript --local > src/types/database.ts ```
Plik `database.ts` zawiera typy dla wszystkich tabel i widoków. Używasz go przy inicjalizacji klienta:
```typescript import { createClient } from '@supabase/supabase-js'; import type { Database } from './types/database';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient
Teraz zapytania są w pełni otypowane:
```typescript // TypeScript wie, że profiles ma pole username, full_name etc. const { data: profiles, error } = await supabase .from('profiles') .select('id, username, full_name') .eq('username', 'jan_kowalski') .single();
// profiles jest typu Database['public']['Tables']['profiles']['Row'] | null ```
Regeneruj typy po każdej migracji. Warto dodać skrypt do `package.json`:
```json { "scripts": { "db:types": "supabase gen types typescript --local > src/types/database.ts" } } ```
Row Level Security: polityki dostępu
RLS (Row Level Security) to mechanizm PostgreSQL, który kontroluje które wiersze użytkownik może odczytać lub modyfikować. Supabase egzekwuje RLS przez JWT — anon key i user session automatycznie trafiają do kontekstu PostgreSQL.
Włączenie RLS na tabeli (domyślnie blokuje wszystko):
```sql alter table public.profiles enable row level security; ```
Polityki przykładowe:
```sql -- Użytkownik może czytać tylko swój profil create policy "Users can read own profile" on public.profiles for select using (auth.uid() = id);
-- Użytkownik może edytować tylko swój profil create policy "Users can update own profile" on public.profiles for update using (auth.uid() = id);
-- Każdy może czytać publiczne profile (przykład otwartej polityki) create policy "Public profiles are readable" on public.profiles for select using (true); ```
`auth.uid()` to funkcja Supabase, która zwraca UUID zalogowanego użytkownika z JWT. Polityki są ewaluowane po stronie bazy danych — nawet jeśli ktoś spreparuje zapytanie, PostgreSQL wymusi limit.
Ważne: tabele bez polityk przy włączonym RLS blokują **wszystkie** operacje. Jeśli zapytanie zwraca pustą tablicę zamiast błędu — sprawdź najpierw czy masz polityki.
Auth: email, magic link, OAuth
**Email i hasło:**
```typescript // Rejestracja const { data, error } = await supabase.auth.signUp({ email: 'jan@example.com', password: 'haslo123', });
// Logowanie const { data, error } = await supabase.auth.signInWithPassword({ email: 'jan@example.com', password: 'haslo123', });
// Wylogowanie await supabase.auth.signOut(); ```
**Magic link (bez hasła):**
```typescript const { error } = await supabase.auth.signInWithOtp({ email: 'jan@example.com', options: { emailRedirectTo: 'https://twoja-aplikacja.pl/auth/callback', }, }); ```
**OAuth z Google:**
W panelu Supabase: Authentication → Providers → Google. Wklej Client ID i Client Secret z Google Cloud Console.
```typescript const { error } = await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: 'https://twoja-aplikacja.pl/auth/callback', }, }); ```
Pobieranie aktualnej sesji:
```typescript const { data: { session } } = await supabase.auth.getSession(); const user = session?.user;
// Nasłuchiwanie zmian stanu auth supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN') { console.log('Zalogowano:', session?.user.email); } if (event === 'SIGNED_OUT') { console.log('Wylogowano'); } }); ```
Storage: buckety, upload, publiczne URL
Storage w Supabase działa jak S3-compatible object store z integracją z Auth i RLS.
Tworzenie bucketu przez klienta (lub przez Dashboard):
```typescript const { data, error } = await supabase.storage.createBucket('avatars', { public: false, // prywatny bucket — dostęp przez polityki fileSizeLimit: 1024 * 1024 * 2, // 2MB allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'], }); ```
Upload pliku:
```typescript const file = event.target.files[0]; // np. z const filePath = `${user.id}/avatar.${file.name.split('.').pop()}`;
const { data, error } = await supabase.storage .from('avatars') .upload(filePath, file, { upsert: true, // nadpisz jeśli plik istnieje }); ```
Publiczny URL (dla publicznych bucketów):
```typescript const { data } = supabase.storage .from('avatars') .getPublicUrl(filePath);
console.log(data.publicUrl); ```
Podpisany URL (dla prywatnych bucketów, ważny przez określony czas):
```typescript const { data, error } = await supabase.storage .from('avatars') .createSignedUrl(filePath, 3600); // ważny 1h ```
Realtime: subscribowanie zmian w tabeli
Supabase Realtime pozwala nasłuchiwać na zmiany w bazie przez WebSockety. Opiera się na PostgreSQL logical replication.
```typescript const channel = supabase .channel('messages-changes') .on( 'postgres_changes', { event: '*', // INSERT | UPDATE | DELETE | * schema: 'public', table: 'messages', filter: 'room_id=eq.123', // opcjonalny filtr }, (payload) => { console.log('Zmiana w tabeli:', payload.eventType); console.log('Nowy rekord:', payload.new); console.log('Stary rekord:', payload.old); } ) .subscribe();
// Odsubskrybuj gdy komponent unmountuje // channel.unsubscribe(); ```
Ważne: tabela musi mieć włączone Realtime w Dashboard (Table Editor → tabela → Realtime) lub przez SQL:
```sql alter publication supabase_realtime add table public.messages; ```
Edge Functions: kiedy używać
Edge Functions to Deno-based serverless funkcje deployowane globalnie blisko użytkownika. Kiedy ich używać:
- Integracje z zewnętrznymi API (webhooks od Stripe, SendGrid, etc.) - Logika, która wymaga service_role key (nie chcesz eksponować go na froncie) - Transformacje danych przed zapisem do bazy - Harmonogramy (cron jobs przez `pg_cron` lub zewnętrzny trigger)
Kiedy **nie** używać Edge Functions: do wszystkiego co można zrobić przez RLS i bezpośrednie zapytania klienta. Edge Functions dodają latencję cold start.
Przykład prostej funkcji:
```typescript // supabase/functions/send-welcome-email/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
serve(async (req) => { const { email } = await req.json();
// Wywołanie zewnętrznego API emailowego await fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { Authorization: `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: [{ email }], from: { email: 'noreply@twoja-aplikacja.pl' }, subject: 'Witaj!', text: 'Dziękujemy za rejestrację.', }), });
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' }, }); }); ```
Checklist produkcyjny
Przed wdrożeniem na produkcję:
**Baza danych:** - Wszystkie tabele mają włączone RLS i zdefiniowane polityki - Indeksy na kolumnach używanych w WHERE i JOIN (`create index on profiles(username);`) - Connection pooling przez pgBouncer włączony (Supabase oferuje to out-of-the-box w trybie Transaction) - Backupy skonfigurowane (daily backups dostępne na płatnych planach)
**Auth:** - Email confirmation włączony dla nowych kont - Redirect URLs ograniczone tylko do Twoich domen - JWT expiry ustawiony świadomie (domyślnie 1h access token, 7 dni refresh)
**Security:** - `service_role` key wyłącznie po stronie serwera (nigdy na froncie) - `anon` key może być publiczny, ale RLS musi go chronić - Row-level security przetestowane dla każdej tabeli - Polityki storage sprawdzone dla każdego bucketu
**Monitoring:** - Logi zapytań włączone w Dashboard → Logs - Alerty na błędy 5xx
Supabase redukuje czas do działającego backendu z tygodni do godzin. PostgreSQL pod spodem oznacza, że nie masz ograniczeń NoSQL — relacje, transakcje, pełny SQL. RLS to jeden z najmocniejszych mechanizmów bezpieczeństwa dostępnych bez pisania własnego middleware. Naucz się go dobrze — to core Supabase.
Potrzebujesz podobnego rozwiązania?
Porozmawiajmy o Twoim projekcie
Pierwsza rozmowa jest bezpłatna. Opisz nam swój pomysł — odpowiemy w ciągu jednego dnia roboczego.
Umów bezpłatną rozmowę