Tilbake til bloggen
Guide12. mars 2026

PostgreSQL: Row-Level Security, migrasjoner og produksjonsdata i Nextbook

Da jeg begynte å bygge Nextbook — læringsplattformen som lar folk lage og selge kurs — visste jeg at databasevalget ville definere hele arkitekturen. Ikke bare ytelsen, men sikkerheten, utvikleropplevelsen og hvor mye jeg kunne stole på systemet mitt. Jeg valgte PostgreSQL, og det er en beslutning jeg ikke har angret på.

Hvorfor PostgreSQL og ikke MongoDB eller Firebase?

Læringsplattformer er fundamentalt relasjonelle. Kurs har kapitler. Kapitler har leksjoner. Leksjoner har quizer. Brukere har påmeldinger, fremgang og resultater. Disse relasjonene er ikke valgfrie — de _er_ datamodellen.

MongoDB er fantastisk for dokumenter uten faste relasjoner, men i Nextbook ville jeg endt opp med å reimplementere JOINs i applikasjonskoden. Firebase/Firestore er raskt å komme i gang med, men sikkerhetreglene lever i et eget konfigurasjonsspråk som raskt blir uoversiktlig når kompleksiteten øker.

PostgreSQL gir meg alt i én pakke: en moden relasjonsmodell, JSONB der jeg trenger fleksibilitet (som kursinnstillinger eller quizdata), et enormt økosystem av extensions, og — kanskje viktigst — Row-Level Security.

Supabase er leveringslaget mitt. Det gir meg Postgres med autentisering, real-time subscriptions og en REST API ut av boksen. Men under panseret er det ren PostgreSQL. Hvis Supabase forsvinner i morgen, kan jeg ta med meg databasen og kjøre den hvor som helst.

Row-Level Security i praksis

Row-Level Security (RLS) er tilgangskontroll på databasenivå. Det er ikke et applikasjonsfilter — det er en regel som PostgreSQL håndhever uansett hvordan du spør etter data.

Hvorfor er dette viktig? Tenk deg at du har en API-rute med en bug som glemmer å filtrere på bruker-ID. Uten RLS får brukeren tilgang til andres data. Med RLS returnerer databasen bare radene brukeren har lov til å se, _uansett hva API-et gjør_.

I Nextbook bruker jeg RLS for alt som handler om tilgang. Her er et reelt eksempel:

-- Brukere kan bare se kurs de er påmeldt i
CREATE POLICY "users_see_own_courses" ON courses
  FOR SELECT
  USING (
    auth.uid() IN (
      SELECT user_id FROM enrollments
      WHERE course_id = courses.id
      AND status = 'active'
    )
  );

-- Kursskapere kan oppdatere sine egne kurs
CREATE POLICY "creators_update_own" ON courses
  FOR UPDATE
  USING (created_by = auth.uid())
  WITH CHECK (created_by = auth.uid());

Den første policyen sørger for at en SELECT på courses-tabellen kun returnerer kurs brukeren er aktivt påmeldt i. Den andre sørger for at bare den som opprettet kurset kan oppdatere det. WITH CHECK validerer også den _nye_ raden etter oppdatering, slik at du ikke kan endre created_by til noen andre.

Et viktig prinsipp: RLS erstatter ikke applikasjonslogikk, men den er det siste forsvarsverket. Applikasjonen min har validering og tilgangskontroll, men RLS sørger for at selv om noe glir gjennom, får ingen tilgang til data de ikke eier.

Migrasjonsstrategi: Versjonerte filer og disiplin

Databasemigrasjoner i produksjon er et tema mange undervurderer. I Nextbook bruker jeg nummererte SQL-filer som kjøres sekvensielt. Ingen ORM-magi, ingen auto-genererte migrasjoner — bare ren SQL som jeg forstår og kan lese om seks måneder.

-- migrations/003_add_course_progress.sql
BEGIN;

CREATE TABLE course_progress (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id),
  course_id UUID NOT NULL REFERENCES courses(id),
  chapter_index INTEGER NOT NULL DEFAULT 0,
  completed_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(user_id, course_id)
);

CREATE INDEX idx_progress_user ON course_progress(user_id);

ALTER TABLE course_progress ENABLE ROW LEVEL SECURITY;

CREATE POLICY "users_own_progress" ON course_progress
  FOR ALL USING (user_id = auth.uid());

COMMIT;

Hvert script er wrappet i en transaksjon. Hvis noe feiler, rulles alt tilbake. Tabeller får alltid RLS aktivert umiddelbart — det er ingen grunn til å vente. Og indekser legges til for kolonner jeg vet vil bli brukt i WHERE-klausuler.

Denne tilnærmingen krever disiplin, men den gir meg full kontroll. Jeg vet nøyaktig hva som skjer med databasen min ved hvert steg, og jeg kan reprodusere hele skjemaet fra scratch ved å kjøre alle migrasjoner i rekkefølge.

Audit trails: Tillit gjennom transparens

For Nextbook er det kritisk å vite hvem som endret hva og når. Kursskapere redigerer innhold, brukere fullfører leksjoner, administratorer kan endre tilganger. Alt dette må spores.

Jeg bruker en generisk audit-trigger som fanger opp alle endringer:

CREATE TABLE audit_log (
  id BIGSERIAL PRIMARY KEY,
  table_name TEXT NOT NULL,
  record_id UUID NOT NULL,
  action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
  old_data JSONB,
  new_data JSONB,
  performed_by UUID REFERENCES auth.users(id),
  performed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, performed_by)
  VALUES (
    TG_TABLE_NAME,
    COALESCE(NEW.id, OLD.id),
    TG_OP,
    CASE WHEN TG_OP != 'INSERT' THEN to_jsonb(OLD) END,
    CASE WHEN TG_OP != 'DELETE' THEN to_jsonb(NEW) END,
    auth.uid()
  );
  RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

SECURITY DEFINER er viktig her — det betyr at trigger-funksjonen kjører med rettighetene til den som opprettet den, ikke brukeren som utløser den. Dette sikrer at audit-loggen alltid kan skrive, selv om brukeren ikke har direkte tilgang til audit_log-tabellen.

For å aktivere dette på en tabell trenger du bare:

CREATE TRIGGER courses_audit
  AFTER INSERT OR UPDATE OR DELETE ON courses
  FOR EACH ROW EXECUTE FUNCTION audit_trigger();

Det fine med denne tilnærmingen er at den er generisk. Jeg legger til én trigger per tabell, og all sporingslogikk er sentralisert i én funksjon. Ingen duplisering, ingen risiko for at noen glemmer å logge endringer.

Erfaringer fra Go og Tuli Technologies

I Tuli Technologies bruker jeg også PostgreSQL, men uten Supabase — direkte med Go og pgx. Erfaringen har lært meg at PostgreSQL er like kraftig uten et abstraksjonssjikt.

I Go skriver jeg SQL direkte med prepared statements. Det er mer verbost enn Supabase sin JavaScript-klient, men det gir total kontroll over queries og connection pooling. RLS bruker jeg ikke i Tuli fordi autentiseringen håndteres annerledes — men prinsippene er de samme: data skal aldri lekke, og endringer skal alltid spores.

En ting jeg har tatt med meg fra Tuli tilbake til Nextbook er viktigheten av EXPLAIN ANALYZE. Når en query begynner å ta mer enn noen millisekunder, åpner jeg query-planen og forstår hva PostgreSQL faktisk gjør. Det er en ferdighet som sparer timer med debugging.

Når du IKKE bør bruke PostgreSQL

PostgreSQL er ikke alltid riktig svar:

  • Embedded/edge-scenarioer: Hvis du trenger en database som kjører i nettleseren eller på en IoT-enhet, er SQLite et bedre valg.
  • Rene dokumentworkloads: Hvis dataene dine ikke har relasjoner og du trenger fleksibelt skjema fra dag én, kan MongoDB være enklere.
  • Rask prototyping: Hvis du bare trenger å validere en idé raskt, kan Firebase/Firestore komme deg raskere i gang — du kan alltid migrere senere.

Men for produksjonssystemer der data har relasjoner, sikkerhet er viktig og du vil eie infrastrukturen din? PostgreSQL er vanskelig å slå.

Oppsummering

PostgreSQL er grunnmuren i både Nextbook og Tuli. RLS gir meg sikkerhet jeg kan stole på. Versjonerte migrasjoner gir meg kontroll. Audit trails gir meg transparens. Og vissheten om at dette er en database som har eksistert i over 25 år — og kommer til å eksistere i 25 til — gir meg trygghet.

Velg teknologi du kan stole på i produksjon. For meg er det PostgreSQL.

#postgresql#database#sikkerhet#nextbook

Nyhetsbrev

Få nye innlegg rett i innboksen.