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

Supabase, otwarty backend oparty o PostgreSQL

Supabase daje PostgreSQL z autoryzacją, plikami i funkcjami w jednym projekcie. Cennik, bezpieczeństwo na poziomie wiersza i typowe błędy.

Supabase, otwarty backend oparty o PostgreSQL

Supabase to zestaw usług zbudowanych wokół zwykłego PostgreSQL: baza danych, autoryzacja użytkowników, magazyn plików, funkcje uruchamiane na żądanie i subskrypcje zmian w czasie rzeczywistym. Kod platformy jest otwarty na licencji Apache 2.0, biblioteka kliencka na MIT, a repozytorium ma ponad sto siedem tysięcy gwiazdek.

Główna różnica wobec konkurencji

Cała reszta wynika z jednej decyzji: pod spodem leży prawdziwy PostgreSQL, a nie własny silnik z własnym językiem zapytań.

Znaczenie tego widać dopiero wtedy, gdy projekt przestaje być prosty. Masz do dyspozycji złączenia, widoki, wyzwalacze, funkcje w bazie, indeksy częściowe i cały ekosystem rozszerzeń, w tym wyszukiwanie pełnotekstowe i pracę z wektorami. Rozwiązania oparte o własny model danych zaczynają w tym momencie wymagać obejść, a tutaj po prostu piszesz zapytanie.

Druga konsekwencja dotyczy wyjścia. Skoro to zwykły PostgreSQL, dane wyeksportujesz standardowym narzędziem i wgrasz gdziekolwiek indziej. To nie jest przewaga marketingowa, tylko realna, bo zmiana dostawcy sprowadza się do przeniesienia bazy i podmiany adresu połączenia, a nie do przepisania warstwy dostępu do danych.

Trzecia to sposób, w jaki działa API. Interfejs udostępniający tabele przez HTTP generuje się automatycznie ze schematu bazy, więc dodanie kolumny natychmiast pojawia się w API bez pisania kodu. Wygodne przy prototypowaniu i ryzykowne później, o czym za chwilę.

Kiedy to jest dobry wybór

Warto nazwać scenariusze wprost, bo ta platforma bywa wybierana odruchowo, a nie każdy projekt na niej zyskuje.

Najlepiej sprawdza się przy produkcie budowanym przez mały zespół, w którym liczy się tempo dojścia do działającej aplikacji. Autoryzacja, baza, pliki i funkcje w jednym miejscu oznaczają, że pierwszy tydzień poświęcasz na produkt, a nie na składanie infrastruktury z klocków. To jest realna oszczędność i główny powód popularności tej platformy.

Drugi dobry scenariusz to projekt, w którym dane są relacyjne i wiadomo, że prędzej czy później pojawi się zapytanie z kilkoma złączeniami albo raport. Wybranie tu bazy dokumentowej oznacza, że ta funkcja powstanie później i gorzej, a przepisanie modelu danych po fakcie jest jedną z droższych rzeczy, jakie można sobie zafundować.

Gorzej wypada w dwóch sytuacjach. Pierwsza to aplikacja z rozbudowaną logiką biznesową po stronie serwera, gdzie i tak potrzebujesz własnej warstwy usług. Wtedy część platformy dotycząca dostępu z przeglądarki jest niepotrzebna, a zostaje hostowany PostgreSQL, który dostaniesz taniej gdzie indziej, na przykład w Neonie.

Druga to wymóg trzymania danych na własnej infrastrukturze. Wariant samodzielnie hostowany istnieje i jest otwarty, ale składa się z kilkunastu usług, więc jego utrzymanie to realna praca, a nie uruchomienie jednego kontenera. Warto to policzyć uczciwie przed decyzją. Przy małym projekcie to samo daje PocketBase, czyli jeden plik wykonywalny z bazą SQLite, uwierzytelnianiem, plikami i API w środku. Ceną za tę prostotę jest pojedynczy proces bez replikacji oraz status przed wersją 1.0, w którym zgodność wsteczna nie jest gwarantowana i aktualizacja bywa ręczna.

Bezpieczeństwo na poziomie wiersza, czyli rzecz krytyczna

To najważniejsza sekcja tego tekstu, bo dotyczy jedynego miejsca, w którym pomyłka kosztuje wyciek danych.

Skoro klient łączy się z bazą bezpośrednio z przeglądarki, to jedyną barierą między użytkownikiem a cudzymi danymi są reguły dostępu zdefiniowane w samej bazie. Nie ma tu warstwy serwera aplikacyjnego, która sprawdzałaby uprawnienia po drodze. Tabela bez włączonych reguł jest widoczna dla każdego, kto zna adres projektu i klucz publiczny, a ten klucz z definicji trafia do przeglądarki.

Objaw problemu jest zdradliwy, bo go nie ma. Aplikacja działa poprawnie, testy przechodzą, a dane są dostępne dla wszystkich. Odkrycie następuje zwykle wtedy, gdy ktoś obcy zajrzy do zakładki sieci w narzędziach przeglądarki.

Praktyczna zasada brzmi więc: włącz reguły dostępu na każdej tabeli od razu po jej utworzeniu, zanim napiszesz choć linijkę kodu klienckiego. Domyślnie zablokowana tabela objawia się pustą listą, co zauważysz natychmiast, w przeciwieństwie do tabeli otwartej.

Druga pułapka dotyczy klucza serwisowego. Obok klucza publicznego istnieje drugi, omijający wszystkie reguły dostępu. Ten należy wyłącznie do kodu serwerowego i jego pojawienie się w zmiennej dostępnej dla przeglądarki jest równoznaczne z oddaniem całej bazy. W Next.js granicą jest prefiks nazwy zmiennej środowiskowej i warto go sprawdzić dwa razy.

Trzecia rzecz to koszt reguł. Reguła jest warunkiem doklejanym do każdego zapytania, więc reguła odwołująca się do podzapytania po innej tabeli wykona się dla każdego wiersza. Przy tysiącu rekordów nikt tego nie zauważy, przy milionie zapytanie przestaje wracać. Jeśli lista nagle zwolniła po dodaniu reguł, to jest pierwsze miejsce do sprawdzenia.

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 i uruchamiane w 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>',
  },
})

Do pojedynczej wiadomości takie wywołanie wystarcza. Gdy dochodzą sekwencje powitalne i przypomnienia przed końcem okresu próbnego, prościej wysłać z funkcji samo zdarzenie do narzędzia pokroju Loops, które trzyma treść i warunki wysyłki u siebie. Rozlicza się ono jednak za liczbę kontaktów, nie wiadomości, więc lista martwych adresów kosztuje tyle samo co aktywna.

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;

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.

Cennik i pułapka planu darmowego

PlanCena miesięcznieBazaPlikiRuch wychodzącyAktywni użytkownicy
Free0 USD500 MB1 GB5 GB50 tys.
Pro25 USD8 GB na projekt100 GB250 GB100 tys.
Team599 USD8 GB na projekt100 GB250 GB100 tys.
Enterprisewycena indywidualnaustalaneustalaneustalaneustalane

Plan płatny zawiera dziesięć dolarów kredytu na moc obliczeniową, co pokrywa najmniejszą instancję, a większa jest dopłatą. Liczba żądań do API jest we wszystkich planach nieograniczona.

Kredyt przypada jednak na organizację, a moc obliczeniowa nalicza się osobno za każdy projekt, i to jest najczęstsze zaskoczenie na tej platformie. Organizacja z jednym projektem na najmniejszej instancji płaci dwadzieścia pięć dolarów, bo kredyt pokrywa całość. Drugi projekt na tej samej instancji podnosi rachunek do trzydziestu pięciu, choć plan się nie zmienił. Przy kilku środowiskach, gdzie osobny projekt służy za wersję testową, policz tę pozycję razy liczbę środowisk, a nie razy jeden.

Najważniejsza rzecz w planie darmowym nie dotyczy jednak limitów, tylko usypiania. Projekty darmowe są wstrzymywane po tygodniu bezczynności i limit aktywnych projektów wynosi dwa. Dla nauki i eksperymentów jest to bez znaczenia, bo projekt wznawia się ręcznie. Dla czegokolwiek, co ma być dostępne dla innych ludzi, jest to warunek dyskwalifikujący: aplikacja pokazana klientowi po dwóch tygodniach przerwy nie odpowie.

Przy szacowaniu kosztów warto zwrócić uwagę na ruch wychodzący, bo to najczęściej niedoszacowana pozycja. Serwowanie obrazów bezpośrednio z magazynu plików zjada ten limit szybciej niż wszystko inne, zwłaszcza gdy pliki nie przechodzą przez sieć dostarczania treści. Jeśli aplikacja rozdaje zdjęcia, policz to osobno, zanim wybierzesz plan.

Typowe błędy

Pierwszy to tabela bez reguł dostępu, opisana wyżej. To jedyny błąd z tej listy, który kończy się wyciekiem danych, a nie awarią.

Drugi to trzymanie połączeń bez puli. PostgreSQL ma twardy limit połączeń jednoczesnych, a środowisko uruchamiające funkcje na żądanie zestawia nowe połączenie przy każdym wywołaniu. Supabase udostępnia osobny adres przechodzący przez pulę i to jego należy używać w kodzie bezstanowym. Migracje schematu i długie zadania administracyjne wymagają natomiast połączenia bezpośredniego, bo tryb transakcyjny puli nie obsługuje wszystkiego.

Trzeci to poleganie na automatycznie generowanym API przy złożonej logice. Interfejs wystawiający tabele świetnie sprawdza się przy odczycie listy i zapisie rekordu, a przestaje wystarczać, gdy jedna operacja musi zmienić trzy tabele atomowo. Do takich rzeczy służą funkcje w bazie wywoływane jako procedury, a nie łańcuch osobnych żądań z przeglądarki, bo ten nie ma żadnej gwarancji spójności.

Czwarty dotyczy subskrypcji zmian w czasie rzeczywistym. Kuszące jest podpięcie ich do wszystkiego, ale każda subskrypcja to otwarte połączenie i przepływ zdarzeń, który trzeba obsłużyć po stronie klienta. Przy liście aktualizowanej raz na godzinę zwykłe odpytanie jest tańsze i prostsze, a mechanizm czasu rzeczywistego zostaw temu, co faktycznie zmienia się na oczach użytkownika.

Piąty to zapominanie o migracjach. Klikanie w panelu jest szybkie i wygodne, a po pół roku nikt nie wie, dlaczego kolumna wygląda tak, jak wygląda, ani jak odtworzyć bazę od zera. Trzymanie zmian schematu w plikach migracji w repozytorium jest tu tym samym, czym trzymanie kodu w systemie kontroli wersji.

Szósty, widoczny dopiero przy pierwszym poważnym ruchu, to brak indeksów pod zapytania, które faktycznie wykonujesz. Baza zakłada indeks na kluczu głównym i na kluczach obcych, ale nie na kolumnie, po której filtrujesz listę ani na kombinacji kolumn użytej w sortowaniu. Przy kilkuset rekordach różnicy nie widać, bo baza i tak przeczyta wszystko, a przy kilkuset tysiącach ta sama strona przestaje się otwierać. Warto raz na jakiś czas przejrzeć najwolniejsze zapytania w panelu i sprawdzić, czy któreś przegląda całą tabelę.

Kod źródłowy znajdziesz w repozytorium Supabase, a aktualne plany na stronie cennika.