Terug naar blog
databasepostgresqlsaas-architectuurmigratiesmulti-tenancyperformance

Databaseontwerp en migraties voor SaaS: van schema tot schaalbare productie

Door SaaS Masters12 maart 20268 min leestijd
Databaseontwerp en migraties voor SaaS: van schema tot schaalbare productie

Elke succesvolle SaaS-applicatie draait om data. Hoe je die data opslaat, structureert en laat meegroeien met je product, bepaalt of je platform soepel schaalt of vastloopt op het slechtst mogelijke moment. In deze gids duiken we diep in databaseontwerp en migratiestrategieën die specifiek relevant zijn voor SaaS-platformen.

Waarom databasekeuzes er toe doen in SaaS

Bij een traditionele webapp maak je één keer een databasekeuze en leef je ermee. Bij SaaS ligt dat anders: je data groeit exponentieel met elke nieuwe klant, je schema moet evolueren zonder downtime, en je moet nadenken over data-isolatie tussen tenants.

Een verkeerde keuze in het begin kan je maanden kosten om te herstellen. Laten we de belangrijkste beslissingen doorlopen.

SQL vs. NoSQL: de genuanceerde werkelijkheid

Wanneer SQL (PostgreSQL, MySQL)

SQL-databases zijn de standaardkeuze voor de meeste SaaS-applicaties, en daar is een goede reden voor:

  • ACID-compliance: transacties zijn betrouwbaar, cruciaal voor facturatie en gebruikersbeheer
  • Complexe queries: joins, aggregaties en rapportages zijn eersteklas features
  • Schema-validatie: de database dwingt datastructuur af
  • Ecosysteem: ORM's zoals Prisma, Drizzle en TypeORM werken uitstekend met SQL
-- Voorbeeld: multi-tenant tabelstructuur met Row-Level Security
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL REFERENCES organizations(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Row-Level Security voor tenant-isolatie
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON projects
  USING (org_id = current_setting('app.current_org')::UUID);

Wanneer NoSQL (MongoDB, DynamoDB)

NoSQL is geen vervanging voor SQL, maar een aanvulling voor specifieke use-cases:

  • Ongestructureerde data: logs, events, gebruikersactiviteit
  • Hoge schrijfvolumes: IoT-data, analytics events
  • Flexibele schema's: wanneer elke tenant andere velden nodig heeft
  • Key-value lookups: caching, sessies, feature flags

De pragmatische aanpak: gebruik PostgreSQL als primaire database en voeg Redis of DynamoDB toe voor specifieke workloads.

Schema-ontwerp voor multi-tenancy

Er zijn drie hoofdpatronen voor multi-tenant databases. Elk heeft trade-offs:

1. Gedeelde database, gedeeld schema (meest voorkomend)

Alle tenants delen dezelfde tabellen, onderscheiden door een org_id kolom.

// Prisma schema voorbeeld
model Project {
  id        String   @id @default(cuid())
  orgId     String
  org       Organization @relation(fields: [orgId], references: [id])
  name      String
  data      Json?
  createdAt DateTime @default(now())
  
  @@index([orgId])
}

Voordelen: eenvoudig, kostenefficiënt, makkelijk te onderhouden
Nadelen: risico op data-lekkage zonder goede filtering, "noisy neighbor"-probleem

2. Gedeelde database, gescheiden schema's

Elke tenant krijgt een eigen PostgreSQL-schema binnen dezelfde database.

-- Tenant-specifiek schema aanmaken
CREATE SCHEMA tenant_acme;
CREATE TABLE tenant_acme.projects (LIKE public.projects INCLUDING ALL);

Voordelen: betere isolatie, makkelijker om per-tenant backups te maken
Nadelen: complexer migratiebeheer, hogere operationele last

3. Database per tenant

Elke tenant krijgt een eigen database-instantie.

Voordelen: maximale isolatie, onafhankelijke scaling
Nadelen: hoge kosten, complex beheer, niet realistisch voor 1000+ tenants

Onze aanbeveling: start met optie 1 (gedeeld schema met org_id) en implementeer Row-Level Security in PostgreSQL. Dit geeft je de eenvoud van een gedeeld schema met de veiligheid van isolatie.

Database-migraties zonder downtime

Migraties zijn het spannendste onderdeel van databasebeheer in productie. Eén verkeerde migratie kan je hele platform platleggen. Hier zijn bewezen strategieën:

De expand-contract pattern

In plaats van destructieve wijzigingen in één stap, splits je ze op:

Stap 1 — Expand: voeg de nieuwe structuur toe naast de oude

-- Voeg nieuwe kolom toe (non-blocking)
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT false;

Stap 2 — Migreer data: vul de nieuwe structuur met bestaande data

-- Backfill in batches om de database niet te overbelasten
UPDATE users SET email_verified = true
WHERE verified_at IS NOT NULL
AND id IN (SELECT id FROM users WHERE email_verified IS NULL LIMIT 1000);

Stap 3 — Contract: verwijder de oude structuur na validatie

-- Pas na volledige validatie
ALTER TABLE users DROP COLUMN verified_at;

Tooling voor veilige migraties

// prisma/migrations gebruiken met een CI/CD check
// In je deployment pipeline:

// 1. Draai migratie tegen een schaduw-database
// 2. Controleer of de migratie reversibel is
// 3. Meet de duur op een kopie van productiedata
// 4. Pas dan deployen naar productie

// Voorbeeld GitHub Actions stap:
// - name: Validate migration
//   run: |
//     npx prisma migrate diff \
//       --from-schema-datasource prisma/schema.prisma \
//       --to-migrations prisma/migrations \
//       --shadow-database-url $SHADOW_DB_URL

Grote tabellen migreren

Bij tabellen met miljoenen rijen moet je slim zijn:

-- SLECHT: dit lockt de hele tabel
ALTER TABLE events ALTER COLUMN payload TYPE JSONB;

-- GOED: maak een nieuwe kolom en migreer geleidelijk
ALTER TABLE events ADD COLUMN payload_v2 JSONB;

-- Trigger voor nieuwe data
CREATE OR REPLACE FUNCTION sync_payload() RETURNS TRIGGER AS $$
BEGIN
  NEW.payload_v2 := NEW.payload::JSONB;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER sync_payload_trigger
  BEFORE INSERT OR UPDATE ON events
  FOR EACH ROW EXECUTE FUNCTION sync_payload();

-- Backfill oude data in batches van 10.000
DO $$
DECLARE
  batch_size INT := 10000;
  affected INT;
BEGIN
  LOOP
    UPDATE events
    SET payload_v2 = payload::JSONB
    WHERE payload_v2 IS NULL
    AND id IN (
      SELECT id FROM events WHERE payload_v2 IS NULL LIMIT batch_size
    );
    GET DIAGNOSTICS affected = ROW_COUNT;
    EXIT WHEN affected = 0;
    PERFORM pg_sleep(0.1); -- Even ademen
  END LOOP;
END $$;

Indexering: de 80/20 regel

De meeste performance-problemen in SaaS-databases komen neer op ontbrekende of verkeerde indexen.

Essentiële indexen voor SaaS

-- 1. Altijd indexeren op tenant-ID
CREATE INDEX idx_projects_org ON projects(org_id);

-- 2. Composite indexen voor veelgebruikte queries
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);

-- 3. Partial indexen voor actieve records
CREATE INDEX idx_users_active ON users(email)
  WHERE deleted_at IS NULL;

-- 4. GIN-index voor JSONB-zoeken
CREATE INDEX idx_settings_data ON settings USING GIN(data);

Indexen monitoren

-- Vind ongebruikte indexen
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexrelname NOT LIKE '%pkey%'
ORDER BY pg_relation_size(indexrelid) DESC;

-- Vind ontbrekende indexen (langzame queries)
SELECT query, calls, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

Connection pooling: een must voor SaaS

Elke tenant genereert database-connecties. Zonder pooling loop je snel tegen limieten aan.

// PgBouncer configuratie (pgbouncer.ini)
// [databases]
// myapp = host=localhost dbname=myapp
//
// [pgbouncer]
// pool_mode = transaction
// max_client_conn = 1000
// default_pool_size = 20
// min_pool_size = 5

// In je Next.js app met Prisma:
// Gebruik een connection string via PgBouncer
// DATABASE_URL="postgresql://user:pass@localhost:6432/myapp?pgbouncer=true"

Voor serverless omgevingen (Vercel, AWS Lambda) is een managed pooler zoals Neon's connection pooler of Supabase's Supavisor essentieel. Zonder pooling maakt elke serverless invocatie een nieuwe connectie — en dat schaalt niet.

Backup- en disaster recovery-strategie

Een goed backup-plan is niet optioneel. Het is onderdeel van je product.

De 3-2-1 regel

  • 3 kopieën van je data
  • 2 verschillende opslagmedia
  • 1 off-site backup
# Geautomatiseerde dagelijkse backup met pg_dump
#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${TIMESTAMP}.sql.gz"

pg_dump $DATABASE_URL | gzip > /backups/$BACKUP_FILE

# Upload naar S3
aws s3 cp /backups/$BACKUP_FILE s3://my-saas-backups/daily/

# Verwijder lokale backups ouder dan 7 dagen
find /backups -name "*.sql.gz" -mtime +7 -delete

Point-in-time recovery

Managed databases zoals Neon, Supabase en AWS RDS bieden point-in-time recovery (PITR). Dit betekent dat je je database kunt terugzetten naar elk moment in de afgelopen X dagen. Zorg dat dit is ingeschakeld.

Checklist: database-ready voor productie

Voordat je live gaat, loop deze checklist door:

  • Tenant-isolatie: elke query filtert op org_id (of RLS is actief)
  • Indexen: alle foreign keys en veelgebruikte WHERE-clausules zijn geïndexeerd
  • Connection pooling: PgBouncer of managed pooler is geconfigureerd
  • Migratiestrategie: expand-contract pattern is gedocumenteerd
  • Backups: dagelijkse backups + PITR ingeschakeld
  • Monitoring: slow query log en pg_stat_statements actief
  • Data-encryptie: at-rest en in-transit encryptie ingeschakeld
  • Soft deletes: belangrijke data wordt nooit hard verwijderd

Conclusie

Databaseontwerp voor SaaS is geen eenmalige keuze — het is een doorlopend proces dat meegroeit met je product. Start eenvoudig met een gedeeld schema en PostgreSQL, implementeer Row-Level Security voor tenant-isolatie, en gebruik de expand-contract pattern voor veilige migraties.

Het belangrijkste advies: meet alles. Gebruik pg_stat_statements, monitor je query-tijden, en optimaliseer pas wanneer de data je vertelt waar het knelpunt zit. Premature optimization is de wortel van alle kwaad — maar negeren van database-performance is de wortel van alle churn.

Wil je hulp bij het opzetten van een schaalbare database-architectuur voor je SaaS? Neem contact met ons op voor een vrijblijvend adviesgesprek.