Tillbaka till blogg
Webbutveckling

Multi-Tenant SaaS-arkitektur 2026 – SĂ„ bygger vi system för 1000+ kunder

2026-01-26
14-16 min
Clickwebb Team

Multi-Tenant SaaS-arkitektur 2026 – SĂ„ bygger vi system för 1000+ kunder

Har du nÄgonsin undrat hur Salesforce, HubSpot eller Slack hanterar miljontals företag i samma databas utan att data blandas? Svaret Àr multi-tenant arkitektur.

NÀr svenska SaaS-bolag vÀxer frÄn 10 till 1000+ kunder uppstÄr frÄgan: Ska varje kund ha egen databas (dyrt) eller dela infrastruktur (komplext)?

Efter att ha byggt produktionssystem för hundratals företag kan vi dela best practices för multi-tenant arkitektur.

Vad Àr Multi-Tenant SaaS?

Multi-tenant = flera kunder (tenants) delar samma applikation och databas.

Motsatsen: Single-tenant dÀr varje kund fÄr egen server och databas (extremt dyrt att underhÄlla).

Tre huvudsakliga approaches

1. Shared Database, Shared Schema (vad vi anvÀnder)

  • Alla tenants i samma databas och tabeller
  • tenant_id kolumn i varje tabell
  • Billigast och enklast att underhĂ„lla

2. Shared Database, Separate Schema

  • Alla tenants i samma databas
  • Varje tenant fĂ„r eget schema (t.ex. tenant_123.*)
  • Mer isolering, svĂ„rare att underhĂ„lla

3. Separate Database

  • Varje tenant fĂ„r egen databas
  • Maximal isolering
  • Extremt dyrt vid 100+ tenants

VÄr rekommendation för 95% av SaaS-bolag: Approach #1 (shared schema).

Varför detta Àr kritiskt för er som bygger SaaS

Vi byggde Talecto - en ATS-plattform som konkurrerar med Greenhouse, Teamtailor och Lever. Valet av multi-tenant arkitektur var inte tekniskt, det var affÀrsmÀssigt.

PÄverkan pÄ er business

Time-to-Market:

  • Single-tenant: 2-4 veckor att provisionera ny kund (server setup, DNS, certifikat)
  • Multi-tenant: < 1 sekund (bara en rad i databasen)

Exempel: NÀr Talecto fÄr ny signup kan de börja anvÀnda systemet DIREKT. Inga vÀntköer, ingen IT-support.

Skalningskostnader:

Antal kunderSingle-tenant kostnad/mÄnMulti-tenant kostnad/mÄnSaving
10 kunder8,000 SEK5,000 SEK37%
100 kunder80,000 SEK8,000 SEK90%
1000 kunder800,000 SEK15,000 SEK98%

Med multi-tenant kan ni prisa aggressivt och ÀndÄ ha 80%+ marginaler.

Vad detta betyder för funding/exit

Investerare bryr sig om unit economics:

Single-tenant:
- Customer Acquisition Cost (CAC): 50,000 SEK
- Monthly hosting per kund: 800 SEK
- Lifetime Value (LTV): 36 mĂ„n × (2,000 SEK - 800 SEK) = 43,200 SEK
- LTV/CAC ratio: 0.86 (dÄligt - under 3.0)

Multi-tenant:
- CAC: 50,000 SEK (samma)
- Monthly hosting per kund: 15 SEK
- LTV: 36 mĂ„n × (2,000 SEK - 15 SEK) = 71,460 SEK
- LTV/CAC ratio: 1.43 (bÀttre, men behöver optimeras)

PoÀng: Multi-tenant ger er runway att vÀxa. Single-tenant brÀnner cash pÄ infrastruktur.

Varför de flesta webbbyrÄer misslyckas med multi-tenant

Typiskt scenario vi ser:

  1. Startup kontaktar "vanlig webbyrÄ" i Stockholm
  2. ByrÄn bygger prototyp med separat databas per kund
  3. Vid 50 kunder: serverdrift kostar mer Àn intÀkterna
  4. Ombyggnad till multi-tenant: 6-12 mÄnader, 500k-1M SEK

Varför hÀnder det?

De flesta webbbyrÄer har erfarenhet av:

  • WordPress-siter (single-tenant by design)
  • Enklare webappar (10-20 anvĂ€ndare max)
  • INTE enterprise SaaS med 1000+ tenants

Red flags nÀr ni intervjuar byrÄer:

  • ❌ "Vi kan anvĂ€nda separate databases - enklare"

  • ❌ "Multi-tenant kan vi lĂ€gga till senare"

  • ❌ "Vi brukar köra varje kund pĂ„ egen server"

  • ✅ "Vi designar shared schema frĂ„n dag 1"

  • ✅ "HĂ€r Ă€r vĂ„r composite foreign key approach"

  • ✅ "Vi har production-erfarenhet med 100+ tenants"

FrÄga direkt: "Har ni byggt ett system som hanterar 500+ företag i samma databas?"

Om svaret Àr nej eller tvekan - de kan inte leverera enterprise SaaS.

Varför Multi-Tenant?

Kostnadsbesparing

Single-tenant exempel (100 kunder):

  • 100 EC2-servrar × 500 SEK = 50,000 SEK/mĂ„nad
  • 100 PostgreSQL-instanser × 300 SEK = 30,000 SEK/mĂ„nad
  • Total: 80,000 SEK/mĂ„nad

Multi-tenant exempel (100 kunder):

  • 2 EC2-servrar (load balanced) × 1,000 SEK = 2,000 SEK/mĂ„nad
  • 1 PostgreSQL RDS × 3,000 SEK = 3,000 SEK/mĂ„nad
  • Total: 5,000 SEK/mĂ„nad

Saving: 93.75%

Enklare underhÄll

Single-tenant:

  • Buggfix? Deploy till 100 servrar
  • Database migration? Kör 100 gĂ„nger
  • Security patch? Uppdatera 100 instanser

Multi-tenant:

  • En deploy
  • En migration
  • En uppdatering

Snabbare onboarding

Ny kund signup:

  • Single-tenant: Provisionera server (10-30 min)
  • Multi-tenant: Skapa rad i database (< 1 sekund)

Database Design för Multi-Tenant

Core Principle: Row-Level Isolation

Varje tabell har company_id:

CREATE TABLE candidates (
  id UUID PRIMARY KEY,
  company_id UUID NOT NULL REFERENCES companies(id),
  name VARCHAR(255),
  email VARCHAR(255),
  resume_url TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  
  -- Index för snabba queries per tenant
  INDEX idx_candidates_company (company_id, created_at DESC)
);

Kritiskt: ALLA queries MÅSTE filtrera pĂ„ company_id:

// ✅ RÄTT
const candidates = await db.candidate.find({
  where: { 
    company_id: currentUser.company_id,
    status: 'active'
  }
});

// ❌ FEL - LĂ€cker data mellan tenants!
const candidates = await db.candidate.find({
  where: { status: 'active' }
});

Foreign Keys med Tenant Context

Problem: Om du bara har application.candidate_id kan Company A referera till Company B's kandidater.

Lösning: Composite foreign keys

CREATE TABLE applications (
  id UUID PRIMARY KEY,
  company_id UUID NOT NULL,
  candidate_id UUID NOT NULL,
  job_id UUID NOT NULL,
  
  -- Composite FK garanterar tenant isolation
  FOREIGN KEY (company_id, candidate_id) 
    REFERENCES candidates(company_id, id),
  
  FOREIGN KEY (company_id, job_id)
    REFERENCES jobs(company_id, id)
);

Nu kan Company A ALDRIG rÄka referera till Company B's data, Àven vid buggar.

Middleware för Automatic Tenant Scoping

AnvÀnd ORM-interceptors för att automatiskt filtrera pÄ tenant:

// Automatiskt injectera company_id i alla queries
@Injectable()
export class TenantInterceptor {
  intercept(context, next) {
    const request = context.getRequest();
    const companyId = request.user.company_id;
    
    // Inject i alla database queries
    request.tenantContext = { company_id: companyId };
    
    return next.handle();
  }
}

Detta förhindrar att utvecklare glömmer filtrera pÄ company_id.

Performance-optimering för Multi-Tenant

Problem: Slow Queries vid Tusentals Tenants

DÄlig query:

-- Skannar miljontals rader
SELECT * FROM candidates 
WHERE company_id = 'abc-123' 
AND status = 'active'
ORDER BY created_at DESC
LIMIT 20;

Execution time: 2-5 sekunder vid 1M+ rader.

Lösning: Compound Index

CREATE INDEX idx_candidates_company_status_created 
ON candidates(company_id, status, created_at DESC);

Execution time: < 10ms.

Partitioning för Extremt Stora Tabeller

Om en tabell vÀxer till 100M+ rader, anvÀnd PostgreSQL partitioning:

-- Partitionera applications per mÄnad
CREATE TABLE applications (
  id UUID,
  company_id UUID,
  created_at TIMESTAMP,
  ...
) PARTITION BY RANGE (created_at);

CREATE TABLE applications_2026_01 
  PARTITION OF applications
  FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE applications_2026_02
  PARTITION OF applications
  FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

Benefit: Queries som filtrerar pÄ datum scannar bara relevant partition.

Connection Pooling

Problem: Varje tenant-request öppnar databas-connection.

Med 1000 samtidiga requests = 1000 connections = database krashar.

Lösning: PgBouncer

// TypeORM config
{
  type: 'postgres',
  host: 'pgbouncer.internal',
  pool: {
    min: 5,
    max: 20  // Max 20 connections frÄn din app
  }
}

PgBouncer hanterar connection pooling och kan serva tusentals requests med 20 faktiska DB-connections.

SĂ€kerhet & Data Isolation

Tenant Isolation i Application Layer

Policy: Ingen raw SQL utan tenant filtering.

// ❌ FÖRBJUDET
await db.query(`SELECT * FROM users WHERE email = '${email}'`);

// ✅ GODKÄNT - anvĂ€nd ORM med automatic tenant scoping
await db.user.findOne({
  where: { 
    company_id: ctx.tenant.id,
    email: email 
  }
});

Database-level Row Security (PostgreSQL)

För extra sÀkerhet, lÀgg till PostgreSQL Row-Level Security:

-- Aktivera RLS
ALTER TABLE candidates ENABLE ROW LEVEL SECURITY;

-- Policy: Users kan bara se sin egen company's data
CREATE POLICY tenant_isolation ON candidates
  USING (company_id = current_setting('app.current_tenant')::uuid);

Nu, Àven om buggy kod glömmer filtrera, returnerar databasen bara tenant's data.

Set tenant i varje request:

await db.query(`SET app.current_tenant = '${user.company_id}'`);

Audit Logging

Logga ALLA queries som accessar kÀnslig data:

@Middleware()
async auditLog(req, res, next) {
  const query = req.query;
  const user = req.user;
  
  await db.auditLog.create({
    company_id: user.company_id,
    user_id: user.id,
    action: 'READ',
    table: 'candidates',
    record_id: query.candidate_id,
    ip_address: req.ip,
    timestamp: new Date()
  });
  
  next();
}

GDPR-krav: Visa kunder exakt vem som accessat deras data.

Skalningsutmaningar i Multi-Tenant System

Problem 1: Noisy Neighbor

Scenario: Company A kör tung export (1M rader). Detta saktar ner Company B's requests.

Lösning: Rate limiting per tenant

const rateLimiter = new RateLimiter({
  keyGenerator: (req) => req.user.company_id,
  max: 100,  // Max 100 requests per minut
  windowMs: 60000
});

Tungt lastande tenants pÄverkar inte andra.

Problem 2: Database Locks

Scenario: Company A's migration lockar tabell, Company B kan inte lÀsa.

Lösning: Background job queues

// LÄnga operationer gÄr via queue
await queue.add('export-candidates', {
  company_id: user.company_id,
  filters: {...}
});

// User fÄr email nÀr klar

Problem 3: Backup & Restore

Utmaning: Hur ÄterstÀller du data för EN tenant utan att pÄverka andra?

Lösning: Point-in-time recovery med tenant filtering

-- Restore specific tenant's data frÄn backup
COPY candidates_backup 
TO candidates
WHERE company_id = 'abc-123' 
AND created_at >= '2026-01-15 10:00:00';

KrÀver regelbundna backups (vi kör var 6:e timme).

Tenant Onboarding Flow

Vad hÀnder nÀr ny kund signup:ar?

Steg 1: Skapa Company Record

const company = await db.company.create({
  id: uuidv4(),
  name: signupData.companyName,
  subdomain: signupData.subdomain,  // tenant123.talecto.com
  plan: 'trial',
  trial_ends_at: addDays(new Date(), 14),
  created_at: new Date()
});

Steg 2: Skapa Admin User

const admin = await db.user.create({
  company_id: company.id,
  email: signupData.email,
  password: await bcrypt.hash(signupData.password, 12),
  role: 'admin'
});

Steg 3: Seed Initial Data

// Skapa default jobb-pipeline
await db.pipeline.create({
  company_id: company.id,
  stages: ['Applied', 'Screening', 'Interview', 'Offer', 'Hired']
});

// Default email-templates
await seedEmailTemplates(company.id);

Total tid: < 500ms.

NÀr ska du INTE anvÀnda Multi-Tenant?

Use Case 1: Extremt kÀnslig data

Exempel: Healthcare (patientjournaler), Banking (transaktioner)

Varför inte: Compliance krÀver fysisk isolering (HIPAA, PCI-DSS).

Lösning: Hybrid - metadata multi-tenant, kÀnslig data i separata databases.

Use Case 2: Kunder vill ha on-premise

Exempel: Stora enterprise-kunder (banks, government)

Varför inte: De vÀgrar dela infrastruktur.

Lösning: Erbjud "self-hosted" version (Docker image de kör sjÀlva).

Use Case 3: Extrem customization per kund

Exempel: Varje kund vill ha unique database schema.

Varför inte: Shared schema blir omöjligt.

Lösning: Kör separate schemas eller databases.

Migrations i Multi-Tenant System

Problem: Hur kör du schema changes?

Single-tenant: Uppdatera varje DB separat.

Multi-tenant: EN migration pÄverkar ALLA tenants.

Best practice: Zero-downtime migrations

Exempel: LĂ€gga till kolumn

-- Steg 1: LĂ€gg till kolumn (nullable)
ALTER TABLE candidates 
ADD COLUMN linkedin_url VARCHAR(255);

-- Deploy kod som populerar kolumnen
-- (kan ske gradvis, ingen downtime)

-- Steg 2: Gör kolumnen NOT NULL (senare migration)
ALTER TABLE candidates 
ALTER COLUMN linkedin_url SET NOT NULL;

Rollback-strategi

Om migration failar:

try {
  await migration.up();
  await db.query('COMMIT');
} catch (error) {
  await db.query('ROLLBACK');
  
  // Alert team
  await slack.send({
    channel: '#alerts',
    message: `Migration failed: ${error.message}`
  });
  
  // Keep old version running
  process.exit(1);
}

Testing Multi-Tenant Logic

Unit Tests med Tenant Context

describe('CandidateService', () => {
  it('should only return tenant candidates', async () => {
    // Setup tenants
    const companyA = await createCompany({ name: 'Company A' });
    const companyB = await createCompany({ name: 'Company B' });
    
    // Create candidates
    await createCandidate({ company_id: companyA.id, name: 'Alice' });
    await createCandidate({ company_id: companyB.id, name: 'Bob' });
    
    // Query as Company A
    const ctx = { tenant: { id: companyA.id } };
    const candidates = await service.getCandidates(ctx);
    
    // Verify isolation
    expect(candidates).toHaveLength(1);
    expect(candidates[0].name).toBe('Alice');
  });
});

Integration Tests: Data Leakage

it('should prevent cross-tenant data access', async () => {
  const companyA = await createCompany();
  const companyB = await createCompany();
  
  const candidateB = await createCandidate({ 
    company_id: companyB.id 
  });
  
  // Try accessing Company B's candidate as Company A
  const ctx = { tenant: { id: companyA.id } };
  
  await expect(
    service.getCandidate(ctx, candidateB.id)
  ).rejects.toThrow('Not found');  // Should not leak existence
});

Monitoring & Observability

Metrics per Tenant

Track prestanda PER tenant:

// Prometheus metrics
const queryDuration = new Histogram({
  name: 'db_query_duration',
  help: 'Database query duration',
  labelNames: ['company_id', 'table']
});

// Instrument queries
const start = Date.now();
const result = await db.query(sql, params);
queryDuration.labels(companyId, 'candidates').observe(Date.now() - start);

Identifiera problem-tenants:

# Top 10 slowaste tenants
topk(10, rate(db_query_duration_sum[5m])) by (company_id)

Alert pÄ Anomalier

// Alert om tenant överskrider normal usage
if (tenant.requestsPerHour > tenant.avgRequestsPerHour * 3) {
  await slack.send({
    channel: '#ops',
    message: `Tenant ${tenant.name} har 3x normal load - möjlig abuse eller bug`
  });
}

Vanliga Misstag att Undvika

Misstag 1: Glömma Tenant-ID i Queries

Symptom: Data leakage mellan kunder.

Fix: Middleware som automatiskt injicerar tenant context (visa tidigare).

Misstag 2: HÄrdkoda Tenant-ID

// ❌ FEL
const candidates = await db.candidate.find({
  where: { company_id: 'hardcoded-uuid' }
});

// ✅ RÄTT
const candidates = await db.candidate.find({
  where: { company_id: ctx.tenant.id }
});

Misstag 3: Ingen Index pÄ company_id

Resultat: Queries som scannar miljontals rader.

Fix: ALLTID index pÄ tenant foreign key.

Misstag 4: Dela Sessions mellan Tenants

// ❌ FEL - session pollution
req.session.lastViewedCandidate = candidateId;

// ✅ RÄTT - namespace sessions
req.session.tenants[ctx.tenant.id].lastViewedCandidate = candidateId;

Kostnadsanalys: Multi-Tenant vs Single-Tenant

Scenario: 500 företagskunder, varje med 50 anvÀndare

Multi-Tenant (vÄr approach)

Infrastructure:

  • 3× AWS EC2 (t3.large): 3,000 SEK/mĂ„n
  • 1× PostgreSQL RDS (db.t3.large): 4,000 SEK/mĂ„n
  • S3 + CloudFront: 1,000 SEK/mĂ„n
  • Total: 8,000 SEK/mĂ„n

Per-tenant kostnad: 16 SEK/mÄn

Single-Tenant

Infrastructure per kund:

  • 1× EC2 (t3.small): 300 SEK/mĂ„n
  • 1× PostgreSQL (db.t3.small): 500 SEK/mĂ„n
  • Per tenant: 800 SEK/mĂ„n

500 tenants × 800 SEK = 400,000 SEK/mĂ„n

Saving med Multi-Tenant: 392,000 SEK/mÄn (98%)

AffÀrsmÀssig ROI: Varför detta Àr vÀrt investeringen

Initialt dyrare att bygga rÀtt:

  • Single-tenant MVP: 300,000 SEK (3 mĂ„nader)
  • Multi-tenant MVP: 450,000 SEK (4-5 mĂ„nader)
  • Extra kostnad: 150,000 SEK

Men efter 100 kunder:

KostnadSingle-tenantMulti-tenantSaving
Initial utveckling300,000 SEK450,000 SEK-150,000 SEK
12 mÄn drift (100 kunder)960,000 SEK96,000 SEK+864,000 SEK
UnderhÄll & bugfixes240,000 SEK120,000 SEK+120,000 SEK
Total efter Är 11,500,000 SEK666,000 SEK834,000 SEK

Break-even: Efter 2 mÄnader med 100 kunder.

Vad svenska SaaS-founders missar

Scenario 1: Bygger single-tenant "för att gÄ snabbt"

  • MĂ„nad 1-3: Snabb MVP, 10 early adopters
  • MĂ„nad 4-8: 50 kunder, driftskostnader = 40,000 SEK/mĂ„n
  • MĂ„nad 9: "Vi mĂ„ste bygga om till multi-tenant"
  • MĂ„nad 10-16: Utveckling stannar, ombyggnad, 500,000 SEK kostnad
  • Resultat: 12 mĂ„naders förlorad tillvĂ€xt

Scenario 2: Bygger multi-tenant frÄn dag 1 (som Talecto)

  • MĂ„nad 1-5: LĂ€ngre MVP-fas
  • MĂ„nad 6-12: Kan skala till 500 kunder utan ombyggnad
  • MĂ„nad 12: Fokuserar pĂ„ features istĂ€llet för infrastruktur
  • Resultat: VĂ€xer ostört

LÀrdomen: Extra 1-2 mÄnader i början sparar 12+ mÄnader senare.

NĂ€r ska ni prata med oss?

RĂ€tt timing att involvera specialister:

  • ✅ Innan ni börjar bygga - Vi designar arkitekturen rĂ€tt frĂ„n start
  • ✅ Vid 20-50 kunder - Innan single-tenant blir ohĂ„llbart
  • ✅ Vid funding-runda - Investerare vill se skalbar tech
  • ❌ Vid 200+ kunder med single-tenant (för sent, extremt dyrt)

Boka strategimöte →

Checklist: Är du redo för Multi-Tenant?

  • Varje tabell har tenant_id / company_id kolumn
  • Composite foreign keys för tenant isolation
  • Middleware som automatiskt filtrerar pĂ„ tenant
  • Index pĂ„ (tenant_id, <query_column>)
  • Row-level security policies (PostgreSQL)
  • Rate limiting per tenant
  • Monitoring med tenant-labels
  • Audit logging av data access
  • Zero-downtime migration strategi
  • Integration tests för data leakage

Slutsats

Multi-tenant arkitektur Àr INTE raketsvetenskap, men krÀver disciplin.

Key takeaways:

  1. Shared schema för 95% av SaaS
  2. Composite foreign keys för sÀkerhet
  3. Automatic tenant scoping förhindrar buggar
  4. Index allt pÄ tenant_id
  5. Test för data leakage

Vi har sett denna approach fungera i production för 100+ företagskunder. Det skalas.


Vill ni bygga er Multi-Tenant SaaS?

Vi pÄ ClickWebb har erfarenhet av att bygga multi-tenant SaaS-plattformar frÄn scratch.

Vi kan hjÀlpa er med:

  • Database design för multi-tenancy
  • Row-level security implementation
  • Performance-optimering för skalning
  • Zero-downtime migrations

BegĂ€r offert →

Relaterade guider:

saasarkitekturdatabasskalbarhetbackendpostgresql

Redo att starta ditt SaaS-projekt?

Kontakta oss för en kostnadsfri konsultation om din SaaS-utveckling

Kontakta oss idag
Multi-Tenant SaaS-arkitektur 2026 – SĂ„ bygger vi system för 1000+ kunder | ClickWebb Blogg | Clickwebb