Skip to content
··18 min de lecture

J'ai construit le multi-tenant au Jour 2. Au Jour 67, je l'ai reconstruit

Je pensais qu'ajouter org_id à chaque table signifiait un multi-tenant blindé. Puis mon audit de sécurité a révélé que les agences écrivaient dans les tables PME — pas à cause d'un bug, mais par conception.

27 octobre 2025, 23h47. Je lance ce que je pensais être un audit de sécurité de routine sur STRAŦUM. Tout a bien fonctionné pendant des semaines — les PME ont leurs données, les agences ont les leurs, le multi-tenant est solide.

Le café est froid. Le script d'audit tourne à travers les logs. Puis je le vois : Les agences écrivaient dans les tables PME.

Pas à cause d'un bug. Pas à cause d'une faille de sécurité. À cause de l'architecture elle-même.

Il y a deux mois, j'avais pris la décision de construire une architecture multi-tenant dès le Jour 2. Un mouvement audacieux pour un fondateur solo avec un seul agent IA fonctionnel. J'avais ajouté org_id à chaque table, écrit des politiques RLS, construit un routage séparé pour les PME et les Agences. Ça fonctionnait — les PME avaient leurs campagnes, les agences avaient leurs clients, les données allaient aux bons endroits.

Ou c'est ce que je croyais.

Je suis resté là probablement 20 minutes à fixer le schéma. Comment j'avais pu rater ça ? J'avais passé des semaines à construire une architecture multi-tenant, à écrire 83 politiques RLS, à tester avec des comptes PME et Agence. Tout *fonctionnait*. Mais "fonctionner" et "être correct" ne sont pas la même chose.

C'est le genre de bug qui te fait remettre en question si tu devrais construire des logiciels tout court. Parce que ce n'est pas une faute de frappe. Ce n'est pas un cas limite manqué. C'est une naïveté architecturale.

J'avais fait l'erreur classique : j'avais supposé que le filtrage org_id suffisait pour l'isolation multi-tenant. Ce n'était pas le cas.

Voici l'histoire de la découverte que la vraie isolation multi-tenant nécessite plus que juste ajouter org_id à chaque table — et les 33 migrations en 48 heures qui ont finalement résolu le problème.

---

> Note : Les exemples SQL dans cet article utilisent des noms de schéma et de table génériques (tenant_b, workspace_entities, entity_data) pour des raisons de sécurité. Les concepts restent les mêmes quelles que soient tes conventions de nommage spécifiques.

---

Le problème : tous les tenants ne sont pas égaux

Voici ce que j'avais initialement construit :

```sql
-- Brand guidelines table (shared by SMEs and Agencies)
CREATE TABLE brand_guidelines (
  id UUID PRIMARY KEY,
  org_id UUID REFERENCES organizations(id),
  name TEXT,
  guidelines JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- RLS policy (seems safe)
CREATE POLICY brand_guidelines_org_isolation ON brand_guidelines
  FOR ALL TO authenticated
  USING (org_id = get_user_org_id());
```

Ça fonctionne parfaitement pour les PME. Chaque organisation a ses propres chartes graphiques. La Row-Level Security garantit qu'elles ne peuvent pas voir les données des autres. (Du moins, je le croyais. Plus là-dessus dans un instant.)

Mais les Agences, c'est différent.

Les agences n'ont pas juste un ensemble de chartes graphiques. Elles en ont une par client :

- Les chartes graphiques du Client A (palette de couleurs vive, typographie audacieuse, messages axés sur l'innovation)

- Les chartes graphiques du Client B (palette de couleurs atténuée, design minimal, positionnement axé sur la qualité)

Même agence, clients différents, marques complètement différentes.

La solution naïve (ce que j'avais d'abord construit) :

```sql
-- Add entity_id to the shared table
ALTER TABLE brand_guidelines ADD COLUMN entity_id UUID;

-- Update RLS policy
CREATE POLICY brand_guidelines_isolation ON brand_guidelines
  FOR ALL TO authenticated
  USING (
    organization_id = get_user_org_id() AND
    (entity_id IS NULL OR entity_id = get_user_entity_id())
  );
```

Problème : Cela créait une table avec deux modèles de données différents :

```
SME row:    organization_id='org-123',  entity_id=NULL,      guidelines={...}
Agency row: organization_id='org-456',  entity_id='entity-a', guidelines={...}
Agency row: organization_id='org-456',  entity_id='entity-b', guidelines={...}
```

Les requêtes devenaient un gâchis avec une gestion complexe de NULL, et chaque fonctionnalité nécessitait une logique "if PME, else Agence" dans le code applicatif.

Et oui, j'ai écrit tout ça avant de réaliser que c'était des symptômes d'un problème plus profond. Des semaines de travail, toutes pointant vers la même conclusion : je m'étais architecturalement coincé dans un coin.

Chaque fonctionnalité nécessitait une logique personnalisée : "Si PME, faire ça. Si Agence, faire autre chose."

Pire, l'architecture faisait des hypothèses incorrectes :

- Les agences qui écrivaient entity_id=NULL pollueraient les données PME

- Les PME ne pouvaient pas avoir de sous-entités même si elles voulaient des sous-comptes

- Le schéma devenait du "gruyère" avec des colonnes nullables

Ce n'était pas une architecture multi-tenant. C'était une seule table essayant de servir deux modèles de données différents.

---

La révélation : différents tenants ont besoin de schémas différents

Vers la fin octobre, j'ai réalisé la vérité : Les PME et les Agences ne partagent pas le même modèle de données.

Modèle de données PME :

```
organization → campaigns → agent_outputs
```

Modèle de données Agence :

```
organization → workspace_entities → campaigns → agent_outputs
                   ↓
            entity_data (e.g., brand guidelines, personas)
```

Les agences ont une couche entière (workspace entities) que les PME n'ont pas. Elles ont aussi une intelligence spécifique à l'entité qui ne devrait pas exister dans le monde PME.

La solution : Schémas de base de données séparés.

```sql
-- SME tables (public schema)
public.brand_guidelines
public.campaigns
public.outputs

-- Agency tables (tenant_b schema)
tenant_b.workspace_entities
tenant_b.entity_data  -- Includes brand guidelines, personas, etc.
tenant_b.campaigns
tenant_b.outputs
```

Maintenant les PME et les Agences ont des tables complètement différentes. Pas de schéma partagé. Pas de pollution de entity_id nullable. Pas de logique "if PME, else Agence".

---

Pourquoi ça compte : le cas business pour le routage de schéma

Avant de plonger dans l'implémentation technique, parlons de pourquoi cette décision architecturale compte au-delà de "c'est du code plus propre."

Future-proofing pour la croissance (peut-être)

Le routage de schéma n'est pas juste pour résoudre le problème d'aujourd'hui. C'est pour garder des portes ouvertes pour des opportunités que je ne peux même pas encore prévoir.

Je suis encore en private alpha avec 15 utilisateurs. Je n'ai pas de clients entreprise. Je n'ai pas parlé à un avocat RGPD. Mais voici ce que le routage de schéma *pourrait* permettre si STRAŦUM grandit :

Expansion internationale :

- Si on s'étend à l'UE : Des schémas séparés pourraient permettre la résidence des données (données clients UE dans le schéma eu_agency sur des serveurs UE)

- Le droit à la suppression devient plus simple : interroge un schéma, au lieu de filtrer dans des tables mixtes

- Pistes d'audit : "Montre-moi toutes les données du Client X" = une requête sur un schéma

Conversations de conformité :

- Quand quelqu'un demande éventuellement "Comment garantissez-vous l'isolation des données ?"

- Avec le filtrage org_id : "Nous utilisons des politiques Row-Level Security" (vague, difficile à vérifier)

- Avec le routage de schéma : "Les données de chaque client vivent dans un schéma de base de données séparé" (concret, auditable)

- Je ne sais pas si ça compte encore. Mais ça *pourrait* compter si on a ces conversations.

La vérité honnête :

Je ne construis pas pour la conformité HIPAA ou SOC 2 en ce moment. Je construis pour des PME et de petites agences qui ont besoin d'une meilleure stratégie marketing.

Mais le routage de schéma signifie que si quelqu'un demande un jour "Pouvez-vous gérer des clients du secteur santé ?" ou "Supportez-vous la résidence des données ?", la réponse est "oui, laissez-moi vous montrer l'architecture" au lieu de "laissez-moi d'abord tout reconstruire."

Les inconvénients (être honnête)

Le routage de schéma n'est pas que des avantages. Voici ce que ça coûte réellement :

Complexité de développement :

- Chaque opération WRITE nécessite une fonction de routage

- Chaque opération READ nécessite une vue de sécurité

- Les tests nécessitent les chemins PME et Agence

- Avec Claude Code : 2 jours de travail intense (27-29 oct. 2025) en soirée

- Sans outils IA : Cela aurait pris des semaines

Risque de migration :

- 33 migrations séquentielles = 33 opportunités de fautes de frappe

- Un seul mauvais ALTER TABLE = corruption des données de production

- J'ai dû exécuter chaque migration 3X en staging avant de toucher la prod

- La paranoïa était réelle

Surcharge de performance des requêtes :

- Des vues avec UNION ALL = des lectures légèrement plus lentes

- Des fonctions de routage = un appel de fonction supplémentaire sur les écritures

- RLS + vues = des plans de requête plus complexes

- (En pratique : je n'ai pas encore remarqué de ralentissements, mais j'ai aussi seulement 15 utilisateurs alpha)

Complexité opérationnelle :

- Les migrations de schéma affectent maintenant 2+ schémas (public + agence)

- Les sauvegardes de base de données nécessitent une restauration consciente du schéma

- Les requêtes de monitoring doivent vérifier plusieurs schémas

- Ça va me causer des problèmes à un moment, je ne sais juste pas quand

Pourquoi j'ai quand même fait ce compromis

La valeur optionnelle pourrait être énorme. Ou elle pourrait ne pas avoir d'importance du tout.

Le routage de schéma garde des portes ouvertes que je ne suis même pas sûr de vouloir franchir :

- Partenariats white-label : Pourrait donner à un partenaire son propre schéma, rebrander l'UI

- Opportunités revendeur : Les agences pourraient revendre avec une isolation des données prouvable

- Différents niveaux de prix : Les clients "Premium" pourraient obtenir des schémas dédiés

- Expansion géographique : Schéma EU, schéma US, schéma APAC - même codebase

Voici le truc : je suis en private alpha. Je ne sais pas si l'une de ces choses comptera. Peut-être que je n'aurai jamais de demande white-label. Peut-être que l'expansion géographique est à des années. Peut-être que l'ensemble du business pivote et que rien de tout ça n'est pertinent.

Mais voici ce que je sais : avec le routage de schéma, ces options existent. Avec le filtrage org_id, la plupart d'entre elles nécessiteraient une réécriture complète.

C'est le pari que j'ai fait : Dépenser 2 jours supplémentaires maintenant (avec Claude Code) pour garder des options ouvertes plus tard.

Est-ce le bon pari ? Demande-moi dans un an.

---

L'architecture : routage de schéma

Pattern 1 : Tables spécifiques au schéma

Certaines tables n'existent que pour un type de tenant :

```sql
-- Specialized tenant schema
CREATE SCHEMA tenant_b;

-- Workspace entities (specific to this tenant type)
CREATE TABLE tenant_b.workspace_entities (
  id UUID PRIMARY KEY,
  organization_id UUID,
  name TEXT,
  metadata JSONB
);

-- Entity-specific data
CREATE TABLE tenant_b.entity_data (
  id UUID PRIMARY KEY,
  organization_id UUID,
  entity_id UUID REFERENCES tenant_b.workspace_entities(id),
  data_type TEXT,
  content JSONB
);
```

Les PME ne touchent jamais ces tables. Elles n'existent pas dans le schéma public.

Pattern 2 : Fonctions de routage de base de données

Comment écrire dans le bon schéma ? Fonctions de routage.

Voici le concept (simplifié) :

```sql
CREATE FUNCTION save_resource_routed(params)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER  -- Run with elevated privileges
AS $$
BEGIN
  -- Step 1: Detect organization type
  SELECT type INTO org_type FROM organizations WHERE id = p_org_id;

  -- Step 2: Route to correct schema based on type
  IF org_type = 'TENANT_B' THEN
    INSERT INTO tenant_b.entity_data (...) VALUES (...);
  ELSE
    INSERT INTO public.brand_guidelines (...) VALUES (...);
  END IF;

  RETURN result;
END;
$$;
```

Comment ça fonctionne :

1. Détecter le type d'organisation : Interroger la table organizations pour déterminer le type de tenant

2. Router vers le bon schéma : Écrire dans le schéma approprié basé sur le type

3. Retourner le résultat : Inclure quel schéma a été utilisé pour le débogage

Code applicatif (identique pour tous les types de tenants) :

```typescript
// Just call the router function - no tenant-specific logic
const result = await supabase.rpc('save_resource_routed', {
  p_org_id: orgId,
  p_entity_id: entityId,  // null for simple tenants
  p_data: { ... }
});
```

Pas de if/else dans le code applicatif. La base de données fait le routage.

Écrire ma première fonction de routage m'a pris 4 heures. Déboguer pourquoi elle ne fonctionnait pas ? 6 heures supplémentaires. Le problème ? J'avais oublié d'accorder les permissions EXECUTE. Énergie de fondateur solo classique : brillance architecturale, négligences de permissions. :P

Pattern 3 : Vues à invocateur de sécurité pour les lectures

Les écritures utilisent des fonctions de routage. Les lectures utilisent des vues.

```sql
-- Unified view combining both schemas
CREATE VIEW resources_unified
WITH (security_invoker = on)  -- Respects RLS policies
AS
  SELECT id, organization_id, NULL AS entity_id, data, 'public' AS source
  FROM public.brand_guidelines
UNION ALL
  SELECT id, organization_id, entity_id, content AS data, 'tenant_b' AS source
  FROM tenant_b.entity_data
  WHERE data_type = 'brand_guidelines';
```

Code applicatif (lectures unifiées) :

```typescript
// Read resources (works for all tenant types)
const { data } = await supabase
  .from('resources_unified')
  .select('*')
  .eq('organization_id', orgId);

// RLS policies filter correctly regardless of source schema
```

Détail clé : WITH (security_invoker = on) garantit que les politiques RLS sont appliquées. Sans ça, les vues contournent RLS (désastre de sécurité).

---

La migration : 33 migrations en 48 heures

Ajouter le routage de schéma n'était pas une seule migration. C'était un voyage.

Tu sais ce qui est fun ? Écrire 33 migrations de base de données d'affilée en sachant que si ne serait-ce qu'UNE contient une faute de frappe, tu vas corrompre les données de production. En fait, "fun" n'est pas le mot. "Terrifiant" est plus précis. J'ai exécuté chaque migration sur staging trois fois avant de toucher la production.

27-29 octobre 2025 : 33 migrations séquentielles pour un routage de schéma complet.

Les phases de migration :

1. Créer le schéma spécialisé - Mettre en place le schéma tenant_b avec les permissions appropriées

2. Créer les tables spécifiques au schéma - Miroir des tables nécessaires dans le nouveau schéma

3. Construire les fonctions de routage - Une pour chaque type de ressource qui a besoin de routage

4. Créer des vues de sécurité - Vues unifiées avec UNION ALL pour les lectures

5. Mettre à jour les politiques RLS - Garantir que les deux schémas ont une isolation correcte

6. Migration des données - Déplacer les données existantes vers les bons schémas

7. Mises à jour de l'application - Passer des requêtes directes aux fonctions de routage/vues

Effort total : 33 migrations, 2 jours avec Claude Code, 100 % ça en valait la peine.

---

Les résultats : vraie isolation multi-tenant

Avant (Tables partagées avec org_id)

Modèle de données :

```sql
public.brand_guidelines (organization_id, entity_id, guidelines)
```

Problèmes :

- ❌ entity_id nullable pour un type de tenant (confusion du modèle de données)

- ❌ Requêtes complexes avec gestion de NULL

- ❌ Logique applicative : if (tenantTypeA) { ... } else { ... }

- ❌ Risque de contamination croisée

Après (Routage de schéma)

Modèle de données :

```sql
public.brand_guidelines (organization_id, guidelines)    -- Tenant Type A
tenant_b.entity_data (organization_id, entity_id, data)  -- Tenant Type B
```

Bénéfices :

- ✅ Modèles de données propres (pas de clés étrangères nullables)

- ✅ Requêtes simples sans gestion complexe de NULL

- ✅ Pas de if/else applicatif (la base de données gère le routage)

- ✅ Contamination inter-schémas impossible (physiquement séparés)

Améliorations de sécurité

Avant : Risque de contamination croisée avec des colonnes nullables et des tables partagées

Après : Les fonctions de routage dirigent automatiquement les écritures vers le bon schéma basé sur le type d'organisation. La séparation physique des schémas rend la contamination croisée impossible.

Niveau d'isolation : Séparation appliquée par la base de données. Pas des vérifications au niveau applicatif.

---

Quand utiliser le routage de schéma vs le filtrage org_id

Toutes les applications multi-tenant n'ont pas besoin du routage de schéma. Utilise cet arbre de décision :

Utilise le filtrage org_id (plus simple) si :

Tous les tenants ont le même modèle de données (ex. application de todos)

Pas de tenancy hiérarchique (pas de sous-entités dans les organisations)

Requêtes simples (WHERE org_id = X fonctionne partout)

B2C ou petit B2B (pas de ventes entreprise, pas d'exigences de conformité)

La vitesse du MVP compte (aller sur le marché en semaines, pas en mois)

Raisonnement business : Tu valides le product-market fit, tu ne construis pas pour les exigences de conformité des entreprises du Fortune 500. Livre vite, refactorise plus tard si tu obtiens de la traction entreprise.

Exemple : Outil de gestion de projet où chaque organisation gère ses propres projets.

```sql
CREATE TABLE projects (
  id UUID PRIMARY KEY,
  org_id UUID,  -- Simple filtering
  name TEXT
);
```

Utilise le routage de schéma (plus complexe) si :

Différents types de tenants ont besoin de modèles de données différents (Type A vs Type B vs Type C)

Tenancy hiérarchique (organizations → workspace_entities → sous-entités)

Ventes entreprise sur la roadmap (Fortune 500, santé, finance, gouvernement)

Conformité réglementaire requise (RGPD, HIPAA, SOC 2, FedRAMP)

Potentiel white-label ou revendeur (les partenaires ont besoin d'une isolation complète des données)

Expansion internationale prévue (exigences de résidence des données)

Raisonnement business : Si tu cibles des clients entreprise, "isolation des données" devient une case à cocher sur les questionnaires de sécurité. Le routage de schéma te permet de répondre avec confiance. Le filtrage au niveau des lignes te laisse à tergiverser.

Exemple : Plateforme où certains tenants ont des structures d'espace de travail hiérarchiques qui diffèrent fondamentalement des clients directs.

Coût/Bénéfice (Mon expérience) :

- Coûts du routage de schéma : 2 jours avec Claude Code (auraient été des semaines sans assistance IA)

- Avantage potentiel : Architecture plus propre, préparation à la conformité, options de partenariat

- Avantage réel : Inconnu - je suis encore en private alpha

- Seuil de rentabilité : Si le routage de schéma ouvre ne serait-ce qu'une porte que je ne pourrais pas franchir autrement, ça se rentabilise

La vraie question : Est-ce que tu optimises pour la vitesse de mise sur le marché ou l'optionnalité ? Les deux sont valides. J'ai choisi l'optionnalité.

---

Stratégies d'isolation alternatives

Le routage de schéma n'est pas la seule approche. Voici le spectre :

Niveau 1 : Bases de données séparées (Isolation maximale)

```

database_tenant_1

database_tenant_2

database_tenant_3

```

Avantages :

- ✅ Isolation physique complète

- ✅ Sauvegardes par tenant

- ✅ Scalabilité indépendante

- ✅ Conformité réglementaire (résidence des données)

Inconvénients :

- ❌ Complexité opérationnelle élevée (gérer N bases de données)

- ❌ Coûteux (instance de base de données par tenant)

- ❌ Requêtes inter-tenants impossibles

- ❌ Migrations de schéma sur toutes les bases de données

Cas d'utilisation : SaaS entreprise avec exigences réglementaires, clients à haute valeur (10k+$/mois).

Niveau 2 : Schémas séparés (Isolation forte)

```
database
├── schema_tenant_1
├── schema_tenant_2
└── public (shared tables)
```

Avantages :

- ✅ Isolation logique forte

- ✅ Infrastructure partagée (une seule base de données)

- ✅ Permissions au niveau du schéma

- ✅ Différents modèles de données par type de tenant

Inconvénients :

- ❌ Plus complexe que le niveau ligne

- ❌ Nécessite des fonctions de routage

- ❌ Complexité de migration (N schémas)

Cas d'utilisation : SaaS B2B avec différents niveaux de clients (approche de STRAŦUM).

Niveau 3 : Filtrage au niveau des lignes avec RLS (Isolation modérée)

```
database
└── public
    └── table (with org_id column)
```

Avantages :

- ✅ Simple à implémenter

- ✅ Migrations faciles (un seul schéma)

- ✅ Analytics inter-tenants possibles

- ✅ Postgres RLS applique l'isolation

Inconvénients :

- ❌ Tous les tenants partagent le même modèle de données

- ❌ Surcharge de performance RLS

- ❌ Risque de mauvaises configurations RLS

Cas d'utilisation : SaaS B2B avec modèles de données uniformes (gestion de projet, CRM).

---

Checklist d'implémentation : routage de schéma

Si tu implémentes le routage de schéma, utilise cette checklist :

Phase 1 : Conception du schéma

- [ ] Créer le discriminateur de type de tenant (organizations.type)

- [ ] Concevoir les tables spécifiques au schéma (qu'est-ce qui va où ?)

- [ ] Créer le schéma spécialisé : CREATE SCHEMA tenant_b;

- [ ] Miroir des tables nécessaires dans le schéma spécialisé

- [ ] Documenter quelles tables vivent dans quel schéma

Phase 2 : Fonctions de routage

- [ ] Écrire une fonction de routage pour chaque type de ressource

- [ ] Utiliser SECURITY DEFINER pour les permissions élevées

- [ ] Définir search_path = public, tenant_b pour l'accès multi-schéma

- [ ] Gérer la détection du type de tenant : SELECT type FROM organizations

- [ ] Retourner les infos de schéma pour le débogage

- [ ] Accorder EXECUTE au rôle authenticated

Phase 3 : Vues de sécurité

- [ ] Créer des vues unifiées pour les lectures (UNION ALL entre les schémas)

- [ ] Utiliser WITH (security_invoker = on) pour l'application des RLS

- [ ] Ajouter une colonne source_schema pour le débogage

- [ ] Tester que les politiques RLS fonctionnent sur les vues

- [ ] Accorder SELECT au rôle authenticated

Phase 4 : Intégration applicative

- [ ] Mettre à jour les écritures pour utiliser les fonctions de routage : supabase.rpc('save_resource_routed', ...)

- [ ] Mettre à jour les lectures pour utiliser les vues : supabase.from('resource_unified').select()

- [ ] Supprimer la logique if/else au niveau applicatif

- [ ] Tester le flux du type de tenant A (écritures dans public)

- [ ] Tester le flux du type de tenant B (écritures dans tenant_b)

- [ ] Vérifier l'isolation des données (Entité A ≠ Entité B)

Phase 5 : Migration et tests

- [ ] Écrire les scripts de migration (30+ pour une couverture complète)

- [ ] Tester sur staging avec des données réalistes

- [ ] Lancer un audit de sécurité (fuites inter-schémas ?)

- [ ] Tests de charge (performance RLS + vues)

- [ ] Surveiller en production (requêtes lentes ?)

---

Leçons apprises

1. org_id est nécessaire, mais pas suffisant

Ajouter org_id à chaque table te donne un filtrage au niveau des lignes. Mais si différents types de tenants ont besoin de modèles de données différents, tu as besoin du routage de schéma.

Apprentissage : "Multi-tenant" n'est pas binaire. Il y a des niveaux d'isolation. J'ai choisi un niveau plus élevé que ce dont j'avais strictement besoin pour mes 15 utilisateurs actuels. Le temps dira si c'était intelligent ou juste du travail supplémentaire.

2. Logique applicative → Logique de base de données

Chaque if (tenantType === 'TYPE_B') dans ton application est une mauvaise odeur de code. Déplace la logique consciente des tenants vers la base de données avec des fonctions de routage.

Apprentissage : Les fonctions de base de données sont plus difficiles à écrire mais potentiellement plus faciles à auditer. Si j'obtiens jamais des clients entreprise qui demandent "prouvez votre isolation des données," je peux pointer vers des procédures stockées. Mais en ce moment, c'est hypothétique.

3. Vues + RLS = Lectures unifiées

Lire depuis plusieurs schémas est complexe. Les vues + security_invoker = on te donnent des lectures unifiées avec une isolation correcte.

Apprentissage : Les vues créent des couches d'abstraction qui pourraient faciliter la conformité un jour. Ou elles pourraient juste ajouter de la complexité dont je n'avais pas besoin. On verra.

4. Les fonctions Security Definer sont puissantes

SECURITY DEFINER permet aux fonctions de s'exécuter avec des privilèges élevés tout en respectant les politiques RLS. Essentiel pour les fonctions de routage.

5. Les migrations en valent la peine (probablement)

33 migrations pour le routage de schéma, c'était beaucoup. Mais le résultat ? Une architecture propre, une vraie isolation, et zéro bug inter-tenants.

Apprentissage : La dette technique a un prix. J'ai choisi de le payer tôt quand j'ai 15 utilisateurs alpha au lieu d'attendre d'avoir des clients payants. C'était le bon choix ? Je le saurai si j'ai jamais 100 clients à gérer.

6. Les failles architecturales font plus de mal que les bugs de code

Trouver une null pointer exception à 23h47 ? Agaçant. Découvrir que toute ton architecture multi-tenant est fondamentalement cassée ? C'est le genre de découverte qui t'empêche de dormir la nuit.

Mais voici ce que j'ai appris : les erreurs architecturales sont réparables. Elles sont chères, oui. Chronophages, absolument. Mais je suis passé de "un type de tenant peut accidentellement contaminer les données d'un autre" à "isolation appliquée par la base de données qu'il est impossible de contourner."

Répare-le tôt, répare-le bien, et tu dormiras mieux.

7. L'architecture, c'est peut-être de la stratégie (ou juste de l'over-engineering)

La décision de routage de schéma n'était pas juste à propos de "code propre." C'était à propos de garder des options futures ouvertes — partenariats white-label, ventes entreprise, expansion internationale.

Mais voici la vérité honnête : je suis en private alpha avec 15 utilisateurs. Je n'ai pas d'entreprises qui frappent à ma porte. Je n'ai pas eu une seule question RGPD. Les partenariats que j'imagine pourraient ne jamais se matérialiser.

Je pensais prendre une décision technique. J'ai peut-être pris une décision de stratégie business. Ou peut-être que j'étais juste en train d'over-engineer parce que je trouve l'architecture de base de données intéressante. :)

Tu as déjà découvert une faille architecturale qui n'était pas un bug mais une erreur de conception ? Comment as-tu géré la reconstruction — tu l'as réparée de façon incrémentale ou tu as tout arraché comme moi ?

Cordialement, Chandler

La série architecture STRAŦUM : C'est la partie 2 du voyage multi-tenant. Ça a commencé par construire le multi-tenant au Jour 2. Après la reconstruction du schéma, j'ai découvert 31 écrans blancs à cause d'un contexte de navigation perdu et que ma base de données était correcte mais 296x trop lente.

---

Tu construis un SaaS multi-tenant avec des besoins d'isolation complexes ? STRAŦUM utilise le routage de schéma pour servir différents types de tenants avec une vraie isolation des données. Demande l'accès alpha sur https://stratum.chandlernguyen.com/request-invitation

---

Apprenant encore que "multi-tenant" a de nombreux niveaux d'isolation. Déboguant encore des politiques RLS à minuit. Remettant encore en question mes décisions architecturales du Jour 2 (mais moins maintenant). Plus d'aventures de base de données sur https://www.chandlernguyen.com/ .

---

Continuer la lecture

Mon parcours
Me suivre
Langue
Preferences