Skip to content

PLAN: Multi-Tenant Consolidation met Medusa RLS

+======================================================================+
| |
| ███╗ ███╗██╗ ██╗██╗ ████████╗██╗ |
| ████╗ ████║██║ ██║██║ ╚══██╔══╝██║ |
| ██╔████╔██║██║ ██║██║ ██║ ██║ |
| ██║╚██╔╝██║██║ ██║██║ ██║ ██║ |
| ██║ ╚═╝ ██║╚██████╔╝███████╗██║ ██║ |
| ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ |
| |
| T E N A N T C O N S O L I D A T I O N P L A N |
| |
| Magic e-VERSE Commerce Platform |
| PostgreSQL RLS + Single Medusa Instance |
| |
| Status: EVALUATIE |
| Prioriteit: Middel |
| Impact: Alle commerce tenants |
| |
+======================================================================+

Dit plan evalueert de haalbaarheid van het consolideren van de huidige 10 afzonderlijke Medusa-containers naar een enkele Medusa-instance met PostgreSQL Row Level Security (RLS), gebaseerd op het open-source project rigby-sh/medusajs-multi-tenancy-ecommerce.


┌──────────────────────────────────────────────────────────────────┐
│ HUIDIGE ARCHITECTUUR │
│ Container-per-Tenant │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ brinxx │ │ bovisales │ │ jodasign │ ... x10 │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ Backend │ │ │ │ Backend │ │ │ │ Backend │ │ │
│ │ │ :4040 │ │ │ │ :4050 │ │ │ │ :4070 │ │ │
│ │ ├─────────┤ │ │ ├─────────┤ │ │ ├─────────┤ │ │
│ │ │Storefront│ │ │ │Storefront│ │ │ │Storefront│ │ │
│ │ │ :10040 │ │ │ │ :10050 │ │ │ │ :10070 │ │ │
│ │ ├─────────┤ │ │ ├─────────┤ │ │ ├─────────┤ │ │
│ │ │ Redis │ │ │ │ Redis │ │ │ │ Redis │ │ │
│ │ │ :6340 │ │ │ │ :6350 │ │ │ │ :6370 │ │ │
│ │ ├─────────┤ │ │ ├─────────┤ │ │ ├─────────┤ │ │
│ │ │Meili │ │ │ │Meili │ │ │ │Meili │ │ │
│ │ │ :7740 │ │ │ │ :7750 │ │ │ │ :7770 │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ PostgreSQL 16 (enkele instance) │ │
│ │ magic_b2b_brinxx │ magic_b2b_bovisales │ ... │ │
│ │ Aparte database per tenant │ │
│ └──────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
#TenantBackendStorefrontRedisMeiliDatabase
1development:4010:10010:6310:7715magic_b2b_development
2demo:4020:10020:6320:7720magic_b2b_demo
3default:4030:10030:6330:7730magic_b2b_default
4brinxx:4040:10040:6340:7740magic_b2b_brinxx
5bovisales:4050:10050:6350:7750magic_b2b_bovisales
6master_magic:4059:6390master_magic
7desluis:4060:10060:6360:7760magic_b2b_desluis
8jodasign:4070:10070:6370:7770magic_b2b_jodasign
9logohorloge:4080:10080:6381:7780magic_b2b_logohorloge
10spranz:4091:10090:6391:7710magic_b2b_spranz

Totaal resources per tenant: 4 containers (backend, storefront, Redis, Meilisearch) Totaal draaiende containers: ~40 containers voor 10 tenants

  • Volledige isolatie — een crash in tenant A raakt tenant B niet
  • Onafhankelijke deployments — per tenant upgraden/downgraden mogelijk
  • Eenvoudige code — geen multi-tenant logica in de applicatie
  • Aparte databases — data-integriteit gegarandeerd op DB-niveau
  • Resource overhead — 40+ containers op één server, veel geheugenverbruik
  • Code sync complexiteit — rsync-pipeline om wijzigingen naar 6+ tenants te pushen
  • Onderhoudslast — elke bugfix moet naar alle tenants gedeployed worden
  • Schaling — elke nieuwe klant = 4 nieuwe containers + nginx config + database
  • Versie drift — PIM op 2.11.3, commerce op 2.13.1, risico op verdere fragmentatie

Voorgestelde Architectuur: RLS Multi-Tenancy

Section titled “Voorgestelde Architectuur: RLS Multi-Tenancy”
┌──────────────────────────────────────────────────────────────────┐
│ RLS ARCHITECTUUR │
│ Single Instance + RLS │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Nginx / Reverse Proxy │ │
│ │ brinxx.magiceverse.online ─────┐ │ │
│ │ bovisales.magiceverse.online ──┤ x-tenant-id │ │
│ │ jodasign.magiceverse.online ───┘ header inject │ │
│ └──────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Medusa Backend (single instance) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Middleware │ │ AsyncLocalStorage │ │ │
│ │ │ x-tenant-id │──│ tenant_id per request │ │ │
│ │ └─────────────┘ └──────────┬───────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────────┐│ │ │
│ │ │ Knex Patch ││ │ │
│ │ │ SET app.current_tenant │◄ │ │
│ │ │ before every query │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────────────┐ │
│ │ PostgreSQL 16 (single database) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Elke tabel: + tenant_id kolom │ │ │
│ │ │ RLS Policy: WHERE tenant_id = current_tenant│ │ │
│ │ │ 44+ tabellen met RLS │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Admin mode: geen header = alle data zichtbaar │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Redis │ │ Meili │ (gedeeld, met tenant prefix) │
│ │ (single) │ │ (single) │ │
│ └───────────┘ └───────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
  1. Middleware laag — Express middleware op alle routes die x-tenant-id uit de HTTP header haalt en opslaat in Node.js AsyncLocalStorage. Tenant ID wordt gevalideerd als UUID.

  2. Framework patch laag — Een patch-package patch op @medusajs/framework die Knex’s connection pool hookt. Vóór elke query wordt SET app.current_tenant = '<tenant-id>' uitgevoerd als PostgreSQL session variable.

  3. Database laag — PostgreSQL Row Level Security policies op 44+ tabellen. Elke tabel krijgt een tenant_id kolom met default current_setting('app.current_tenant'). SELECT/INSERT/UPDATE/DELETE policies filteren automatisch op de actieve tenant.


Vergelijking: Container-per-Tenant vs. RLS

Section titled “Vergelijking: Container-per-Tenant vs. RLS”
AspectContainer-per-Tenant (huidig)RLS Single Instance
Backend processes10 Medusa instances1 Medusa instance
Redis instances101 (met tenant prefix)
Meilisearch instances91 (met tenant index)
Storefront instances9 Next.js apps1-2 (met tenant routing)
Databases10 aparte databases1 gedeelde database
RAM schatting~20-30 GB~4-6 GB
Nieuwe tenant toevoegen4 containers + nginx + DB1 DB migratie + nginx

Drastisch minder resources

Van ~40 containers naar ~4. Significante besparing op RAM en CPU, vooral relevant bij groei naar 20+ tenants.

Eén deployment

Bugfixes en features worden direct voor alle tenants beschikbaar. Geen rsync-pipeline meer nodig.

Database-level isolatie

RLS wordt door PostgreSQL zelf afgedwongen. Kan niet omzeild worden door applicatiefouten of SQL-injectie.

Eenvoudiger onboarding

Nieuwe tenant = UUID aanmaken + seed data draaien. Geen container-orchestratie nodig.

Fragiele framework patch

De Knex-patch target een specifieke Medusa versie (2.10.1). Bij elke Medusa upgrade moet de patch opnieuw gevalideerd en aangepast worden. Dit is het grootste risico.

Gedeeld risico (blast radius)

Eén crash = alle tenants down. Geen mogelijkheid om één tenant terug te rollen zonder alle tenants te raken.

APLT module compatibiliteit

Onze custom APLT B2B module (producten, technieken, prijzen) moet volledig RLS-compatible gemaakt worden. Alle custom tabellen hebben tenant_id nodig.

Superuser restrictie

PostgreSQL superusers bypassen RLS volledig. De applicatie MOET draaien als non-superuser. Migraties moeten apart als superuser draaien — dit vereist wijziging in het deploy-proces.

RisicoImpactKansMitigatie
Medusa upgrade breekt patchHoog — alle tenants onbereikbaarHoog — bij elke upgradePin Medusa versie, test patch vóór upgrade in staging
Data leak tussen tenantsKritiek — privacy/complianceLaag — RLS is robuustNon-superuser afdwingen, integration tests per release
Single point of failureHoog — alle tenants tegelijk downMiddelHealth checks, auto-restart, DB replicatie
APLT module incompatibelHoog — custom business logicMiddelUitgebreide audit + test suite voor APLT tabellen
Performance degradatieMiddel — trage queriesLaagtenant_id indexes, query plan monitoring
Tenant-specifieke customizationsMiddel — per-tenant features onmogelijkHoogFeature flags systeem bouwen

ComponentHuidigNa RLS
Backend code syncrsync naar 6+ tenantsNiet meer nodig — single codebase
Storefront code syncrsync naar 6+ tenantsStorefront routing per tenant nodig
Docker compose files10 aparte files1 docker-compose
Nginx configs20+ site configsVereenvoudigd, maar tenant-to-UUID mapping nodig
Database migratiesPer tenant apartEenmalig, maar complexer (RLS policies)

De custom APLT B2B module bevat tabellen die tenant_id nodig hebben:

aplt_products → tenant_id + RLS policies
aplt_techniques → tenant_id + RLS policies
aplt_product_variants → tenant_id + RLS policies
aplt_buying_prices → tenant_id + RLS policies
aplt_product_images → tenant_id + RLS policies (of gedeeld via PIM?)

De huidige storefronts zijn identieke Next.js apps per tenant met tenant-specifieke environment variables (kleuren, logo, API keys). Bij RLS consolidatie zijn er twee opties:

Optie A: Single Storefront met tenant routing

  • Eén Next.js app die op basis van het domein de tenant bepaalt
  • x-tenant-id header meesturen bij alle Medusa API calls
  • Tenant-specifieke theming via database of config

Optie B: Aparte storefronts behouden

  • Storefronts blijven per tenant (ze zijn lightweight)
  • Alleen de backend + Redis + Meili worden geconsolideerd
  • Minder complex, behoud van per-tenant deploy flexibiliteit

  1. 10 tenants is beheersbaar — de container-overhead is merkbaar maar niet kritiek
  2. De Knex-patch is fragiel — Medusa v2 is nog in actieve ontwikkeling, frequente updates
  3. APLT module is complex — veel custom tabellen die allemaal RLS nodig hebben
  4. PIM-integratie — het huidige sync-model werkt, RLS vereist significant redesign
  5. Geen officiële Medusa multi-tenancy support — dit is een community-oplossing
  1. Fase 1: Consolideer gedeelde services

    • Reduceer van 10 naar 1-2 Redis instances (met key prefixing)
    • Reduceer van 9 naar 1-2 Meilisearch instances (met tenant indexes)
    • Dit bespaart al ~16 containers zonder applicatiewijzigingen
  2. Fase 2: Standaardiseer code deployment

    • Vervang rsync door een Git-based deployment (zie Git Integration Plan)
    • Alle tenants op dezelfde commit, maar eigen container
    • Verwijder de noodzaak voor handmatige code sync
  3. Fase 3: Evalueer RLS voor nieuwe tenants (optioneel)

    • Bij groei naar 20+ tenants: implementeer RLS voor een aparte “pool” van kleine/standaard tenants
    • Houd grotere tenants (brinxx, bovisales) op eigen containers
    • Test de RLS-aanpak eerst uitgebreid met development en demo tenants
  4. Fase 4: Monitor Medusa’s eigen multi-tenancy (afwachten)

    • Medusa v2 is nog in ontwikkeling — officiële multi-tenancy support is mogelijk
    • Volg de Medusa roadmap voor updates
    • Een officiële oplossing zou de patch-afhankelijkheid elimineren

Onafhankelijk van de multi-tenancy beslissing, kunnen deze optimalisaties direct uitgevoerd worden:

Terminal window
# Huidige situatie: 10 Redis containers
# Voorstel: 1 Redis instance met database nummering
# Per tenant een aparte Redis database (0-15 beschikbaar)
# brinxx: redis://redis:6379/0
# bovisales: redis://redis:6379/1
# jodasign: redis://redis:6379/2
# etc.
Terminal window
# Huidige situatie: 9 Meilisearch containers
# Voorstel: 1 Meilisearch instance met tenant-prefixed indexes
# brinxx: products_brinxx, categories_brinxx
# bovisales: products_bovisales, categories_bovisales
# etc.

Als We Toch RLS Willen: Implementatie Checklist

Section titled “Als We Toch RLS Willen: Implementatie Checklist”
Pre-implementatie:
[ ] Medusa versie pinnen op een specifieke release
[ ] patch-package valideren tegen onze Medusa versie (2.13.1)
[ ] APLT module audit: alle custom tabellen identificeren
[ ] PIM sync-flow documenteren en redesign plannen
[ ] Non-superuser PostgreSQL setup testen
[ ] Staging omgeving inrichten met RLS
Implementatie:
[ ] PostgreSQL non-superuser (medusa_app_user) aanmaken
[ ] RLS migratie schrijven voor alle 44+ Medusa tabellen
[ ] RLS migratie schrijven voor APLT tabellen
[ ] Middleware implementeren (x-tenant-id uit domein)
[ ] Knex patch toepassen en testen
[ ] Data migratie script: 10 databases → 1 database met tenant_id
[ ] Nginx aanpassen: domein → tenant UUID mapping
[ ] Storefront routing implementeren (Optie A of B)
[ ] PIM sync aanpassen voor tenant_id
Validatie:
[ ] Integration tests per tenant
[ ] Cross-tenant data leak test
[ ] Performance benchmark (10 tenants in 1 DB)
[ ] Failover en recovery test
[ ] Admin dashboard test (geen tenant = alle data)

Bij de hybride aanpak (Fase 1-2) is rollback eenvoudig:

  • Redis/Meilisearch consolidatie: start de individuele containers weer op
  • Git deployment: val terug op rsync

Bij volledige RLS migratie is rollback complex:

  • Database moet gesplit worden naar individuele databases
  • Alle containers opnieuw opzetten
  • Aanbeveling: behoud de oude databases minimaal 3 maanden als backup