Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik11 min czytania

Supabase

Supabase to open-source alternatywa dla Firebase z PostgreSQL, autentykacją, storage i real-time. Kompletny przewodnik z przykładami kodu.

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

  1. Załóż konto na supabase.com
  2. Stwórz nowy projekt
  3. Zapisz URL i klucze API

Instalacja SDK

Code
Bash
# JavaScript/TypeScript
npm install @supabase/supabase-js

# React specyficzne hooki
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs

Konfiguracja klienta

TSlib/supabase.ts
TypeScript
// 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
ENV
# .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:

Code
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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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)

Code
TypeScript
// 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, notion

Magic Link (Passwordless)

Code
TypeScript
const { error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://myapp.com/auth/callback',
  },
})

Sesja użytkownika

Code
TypeScript
// 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

TSapp/auth/callback/route.ts
TypeScript
// 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))
}
TSmiddleware.ts
TypeScript
// 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.

Code
SQL
-- 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
SQL
-- 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

Code
TypeScript
// 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)

Code
TypeScript
// 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)

Code
TypeScript
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):

TSsupabase/functions/send-email/index.ts
TypeScript
// 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:

Code
TypeScript
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

Code
Bash
# 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.ts

Użycie wygenerowanych typów:

Code
TypeScript
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 | null

Porównanie Supabase vs Firebase

CechaSupabaseFirebase
Baza danychPostgreSQL (relacyjna)Firestore (NoSQL)
Open SourceTakNie
Self-hostingTakNie
SQLPełne wsparcieBrak
RelacjeForeign keys, JOINsBrak
Real-timePostgreSQL changesFirestore snapshots
AuthPodobne możliwościPodobne możliwości
StoragePodobne możliwościPodobne możliwości
Edge FunctionsDenoNode.js
PricingBardziej przejrzystyPay-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

Code
SQL
-- Nigdy nie zostawiaj tabel bez RLS w produkcji!
ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;

2. Używaj typów TypeScript

Code
Bash
# Regeneruj typy po każdej zmianie schematu
supabase gen types typescript --project-id xxx > types/database.types.ts

3. Optymalizuj zapytania

Code
TypeScript
// Wybieraj tylko potrzebne kolumny
const { data } = await supabase
  .from('posts')
  .select('id, title, slug') // Nie select('*')
  .limit(10)

4. Używaj indeksów

Code
SQL
-- 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ść