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_idkolumn 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 kunder | Single-tenant kostnad/mÄn | Multi-tenant kostnad/mÄn | Saving |
|---|---|---|---|
| 10 kunder | 8,000 SEK | 5,000 SEK | 37% |
| 100 kunder | 80,000 SEK | 8,000 SEK | 90% |
| 1000 kunder | 800,000 SEK | 15,000 SEK | 98% |
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:
- Startup kontaktar "vanlig webbyrÄ" i Stockholm
- ByrÄn bygger prototyp med separat databas per kund
- Vid 50 kunder: serverdrift kostar mer Àn intÀkterna
- 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:
| Kostnad | Single-tenant | Multi-tenant | Saving |
|---|---|---|---|
| Initial utveckling | 300,000 SEK | 450,000 SEK | -150,000 SEK |
| 12 mÄn drift (100 kunder) | 960,000 SEK | 96,000 SEK | +864,000 SEK |
| UnderhÄll & bugfixes | 240,000 SEK | 120,000 SEK | +120,000 SEK |
| Total efter Är 1 | 1,500,000 SEK | 666,000 SEK | 834,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)
Checklist: Ăr du redo för Multi-Tenant?
- Varje tabell har
tenant_id/company_idkolumn - 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:
- Shared schema för 95% av SaaS
- Composite foreign keys för sÀkerhet
- Automatic tenant scoping förhindrar buggar
- Index allt pÄ tenant_id
- 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
Relaterade guider: