SaaSSupabasePostgreSQL+2 więcej

Makerkit #4 - Bazy Danych. Supabase, RLS i Modelowanie Danych

Backend to nie tylko API. Naucz się projektować bezpieczne schematy w PostgreSQL, zarządzać migracjami w Supabase i pisać zaawansowane polityki RLS, które pilnują nie tylko dostępu, ale i limitów biznesowych Twojego SaaS-a.

/
5 min czytania
Makerkit #4 - Bazy Danych. Supabase, RLS i Modelowanie Danych
Schemat bazy danych i RLS - fundamenty bezpieczeństwa SaaS.

Witajcie w czwartym odcinku naszej serii. 👋

Do tej pory bawiliśmy się gotowymi klockami: konfigurowaliśmy kolory, uruchamialiśmy kontenery. Dziś wchodzimy do kendu i projektujemy Bazę Danych.

W świecie Makerkit (i nowoczesnego Next.js) baza danych to nie tylko "pojemnik na tekst". Supabase (oparty na PostgreSQL) przejmuje na siebie ogromną część logiki:

  1. Relacje między danymi (Schema).
  2. Bezpieczeństwo (Row Level Security - RLS).
  3. Logikę biznesową (Functions & Triggers).

Zrozumienie tego jest kluczowe, aby Twój SaaS był skalowalny i bezpieczny. Zapnijcie pasy, wchodzimy w SQL. 💾

🔄 Cykl Życia Pracy Developera (The Workflow)

Wielu początkujących popełnia błąd, próbując dokonywać zmian, klikając w panelu Supabase w przeglądarce ("Dashboard"). Nie rób tego. W profesjonalnym projekcie pracujemy lokalnie, używając migracji.

Cykl pracy w Makerkit wygląda tak:

  1. Edycja: Tworzysz plik migracji SQL.
  2. Reset: Aplikujesz zmiany do lokalnej bazy (pnpm run supabase:web:reset).
  3. Typegen: Generujesz typy TypeScript na podstawie bazy (pnpm run supabase:web:typegen).
  4. Kodowanie: Używasz nowych, w pełni otypowanych tabel w Next.js.

Dzięki temu Twoja baza danych jest zawsze zsynchronizowana z kodem, a TypeScript pilnuje, czy nie próbujesz pobrać nieistniejącej kolumny.

🏗️ Projektowanie Schematu (Schema Design)

Sercem Makerkita jest tabela public.accounts. To ona trzyma wszystko w ryzach. Każdy element Twojego systemu (np. Projekty, Faktury, Zgłoszenia) musi należeć do jakiegoś Konta (które może być użytkownikiem indywidualnym LUB zespołem).

Stwórzmy przykładowy system Ticketów Supportowych (to idealny przykład relacji).

1. Tworzenie Migracji

W terminalu wpisujemy:

pnpm --filter web supabase migration new support-schema

To utworzy pusty plik SQL w folderze apps/web/supabase/migrations.

2. Definicja Tabeli

Oto jak wygląda profesjonalna definicja tabeli w SQL. Zauważ użycie ENUM do statusów – to zapobiega literówkom w kodzie.

-- Definiujemy statusy (lepsze niż zwykły string!)
create type public.ticket_status as enum ('open', 'closed', 'resolved', 'in_progress');

create table if not exists public.tickets (
  id uuid primary key default gen_random_uuid(),
  -- KLUCZOWE: Powiązanie z kontem (Właścicielem danych)
  account_id uuid not null references public.accounts(id) on delete cascade,
  title varchar(255) not null,
  status public.ticket_status not null default 'open',
  -- Kto stworzył/przypisał ticket?
  assigned_to uuid references public.accounts(id) on delete set null,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

-- Indeks przyspiesza wyszukiwanie po koncie (bardzo ważne!)
create index ix_tickets_account_id on public.tickets(account_id);

🛡️ RLS: Twój Osobisty Ochroniarz

Teraz najważniejsza część. Domyślnie w Supabase każdy może zrobić wszystko (jeśli ma klucz API). Musimy włączyć Row Level Security (RLS).

RLS to mechanizm, w którym baza danych sprawdza każdy wiersz przed jego wydaniem. To jak ochroniarz w klubie, który sprawdza dowód każdemu gościowi z osobna.

Krok 1: Zero Trust (Odbieramy uprawnienia)

Najpierw blokujemy wszystko.

revoke all on public.tickets from public, service_role;

Krok 2: Otwieramy drzwi dla zalogowanych

grant select, insert, update, delete on public.tickets to authenticated;
grant select, insert on public.tickets to service_role;

Krok 3: Włączamy RLS i Definiujemy Politykę

Poniższa polityka mówi: "Użytkownik może zobaczyć ten bilet TYLKO WTEDY, gdy należy do konta, które jest właścicielem biletu".

alter table public.tickets enable row level security;

create policy select_tickets
  on public.tickets
  for select
  to authenticated
  using (
    -- Funkcja sprawdzająca przynależność
    public.has_role_on_account(account_id)
  );

👮‍♂️ Poziom Hard: Granularne Uprawnienia (RBAC)

Makerkit idzie o krok dalej. Co jeśli chcesz, aby każdy w firmie widział tickety, ale tylko Właściciel (Owner) mógł je usuwać? Zwykłe has_role_on_account tu nie wystarczy.

Musimy użyć systemu uprawnień Makerkita (public.role_permissions).

  1. Dodajemy uprawnienie do systemu:
    alter type public.app_permissions add value 'tickets.delete';
    
  2. Przypisujemy uprawnienie do roli Owner:
    insert into public.role_permissions(role, permission)
    values ('owner', 'tickets.delete');
    
  3. Tworzymy restrykcyjną politykę RLS:
    create policy delete_tickets
      on public.tickets
      for delete
      to authenticated
      using (
        -- Sprawdzamy nie tylko konto, ale i konkretne uprawnienie!
        public.has_permission(auth.uid(), account_id, 'tickets.delete'::app_permissions)
      );
    

To jest poziom Enterprise. Dzięki temu Twoja aplikacja jest gotowa na duże firmy, które mają skomplikowane struktury pracowników.

🧠 Logika w Bazie: Funkcje i Triggery

PostgreSQL pozwala na automatyzację. Zamiast pisać w kodzie JS "kiedy status zmieni się na zamknięty, ustaw datę zamknięcia", zróbmy to w bazie. To gwarantuje spójność danych.

Ale możemy robić rzeczy znacznie ciekawsze – np. pilnowanie limitów planu (Billing). Wyobraź sobie funkcję check_ticket_limit, która uruchamia się PRZED dodaniem nowego ticketa:

create trigger check_ticket_limit
before insert on public.tickets
for each row
execute function public.check_ticket_limit();

Taka funkcja (opisana w dokumentacji Makerkit) sprawdza, czy firma ma wykupioną subskrypcję i czy nie przekroczyła limitu np. 50 ticketów miesięcznie. Jeśli tak – baza danych odrzuci zapis i zwróci błąd. Bezpieczeństwo absolutne.

🌱 Seeding: Dane na Start

Praca na pustej bazie jest trudna i ciężka do zrozumienia (przypomina się nauka z książek lub wykładów o programowaniu, gdzie poziom abstrakcji pokonuje nawet najbardziej tęgie umysły). Makerkit używa pliku seed.sql w folderze supabase/, aby wgrać dane startowe przy każdym resecie bazy.

Warto dodać tam przykładowe tickety:

INSERT INTO public.tickets (account_id, title, status, priority)
VALUES
  ('id-twojego-konta', 'Problem z logowaniem', 'open', 'high'),
  ('id-twojego-konta', 'Błąd na fakturze', 'in_progress', 'medium');

Dzięki temu po wpisaniu pnpm run supabase:web:reset, od razu masz aplikację pełną danych do testowania UI.

⌨️ Typegen: Magia TypeScripta

Po nałożeniu migracji, czas na nagrodę. Uruchamiamy:

pnpm run supabase:web:typegen

Ten skrypt skanuje Twoją lokalną bazę Postgres i generuje plik database.types.ts. Teraz w kodzie aplikacji Next.js masz pełne podpowiadanie typów. TypeScript wie, że status może być tylko 'open' | 'closed', a nie dowolnym stringiem.

💡 Podsumowanie

Praca z bazą danych w Makerkit to coś więcej niż CREATE TABLE. To budowanie bezpiecznej twierdzy, gdzie:

  1. Schema definiuje strukturę i relacje.
  2. RLS i RBAC pilnują, by nikt nie zobaczył (ani nie usunął!) danych bez uprawnień.
  3. Triggery dbają o automatyzację i pilnują limitów biznesowych (Billing).
  4. Typegen spina to wszystko z Twoim frontendem w Next.js.

Mając tak solidne fundamenty, możemy przejść do budowania interfejsu użytkownika, wiedząc, że nasze dane są bezpieczne. 🛡️

W następnym odcinku zajmiemy się Server Components i pobieraniem tych danych na frontendzie!


Źródło: Makerkit Course - Database Schema and Migrations