Architecture Forge

0DATA · Juillet 2026 · Document technique — Source de vérité

1. Isolation Tenant — Vue d'Ensemble

┌──────────────────────────────────────────────────────────┐ │ FORGE 0DATA — Infra Mère │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌───────────┐│ │ │ TENANT A │ │ TENANT B │ │ TENANT C ││ │ │ MSP Systel IT │ │ Cabinet Conseil │ │ Client Dir.││ │ │ ─────────────────│ │ ─────────────────│ │ ───────────││ │ │ │ │ │ │ ││ │ │ DB: tenant_a │ │ DB: tenant_b │ │ DB:tenant_c││ │ │ SPINA: chain-A │ │ SPINA: chain-B │ │ SPINA: C ││ │ │ NOVA: :5100-a │ │ NOVA: :5100-b │ │ NOVA:5100-c││ │ │ cockpit/systel │ │ cockpit/cabinet │ │ cockpit/c ││ │ │ domain: systel.fr│ │ domain: conseil │ │ cockpit C ││ │ │ ─────────────────│ │ ─────────────────│ │ ───────────││ │ │ 🔒 ISOLÉ │ │ 🔒 ISOLÉ │ │ 🔒 ISOLÉ ││ │ └─────────────────┘ └─────────────────┘ └───────────┘│ │ │ │ Nginx (443) → route par Host header → bon tenant │ │ NATS → sujets préfixés par tenant_id │ │ PostgreSQL → Row-Level Security par organizationId │ │ SPINA → chaînes séparées, bloc genesis unique/tenant │ │ NOVA → processus isolés, ports dédiés par tenant │ └──────────────────────────────────────────────────────────┘

2. Couches d'Isolation

CoucheMécanismeGranularitéFuite possible ?
DNS / Nginxserver_name → proxy tenantPar domaineNon — Host header
PostgreSQLorganizationId + Row-Level SecurityPar ligneNon — RLS enforce
NATSPréfixe tenant.{id}.* sur tous les sujetsPar messageNon — NATS ACLs
SPINAChaîne blockchain séparée par tenantPar chaîneImpossible — hash incompatible
NOVA (processus)Ports dédiés, répertoire data/ séparéPar processusNon — bind 127.0.0.1
Cockpit (frontend)JWT with tenant claim, API filterPar tokenNon — middleware vérifie
FichiersRépertoire /opt/nova/tenants/{id}/Par dossierNon — permissions UNIX
Certificats SSLLet's Encrypt par domaine (SNI automatique)Par domaineNon — TLS SNI
LogsFichier séparé /var/log/nova/{tenant}.logPar fichierNon — permissions 640

3. Isolation PostgreSQL — Row-Level Security

Principe

Une seule base PostgreSQL. Chaque table a une colonne organizationId. Row-Level Security (RLS) empêche un tenant de lire ou écrire les données d'un autre tenant. Le middleware Forge injecte organizationId dans chaque requête.

-- Activation RLS par table
ALTER TABLE "Service" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Client" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Invoice" ENABLE ROW LEVEL SECURITY;

-- Politique : lire SEULEMENT ses propres lignes
CREATE POLICY tenant_isolation ON "Service"
  FOR ALL
  USING ("organizationId" = current_setting('app.current_tenant_id')::uuid);

-- Le middleware Forge définit le tenant avant chaque requête :
-- SELECT set_config('app.current_tenant_id', $tenantId, true);

RLS est activé au niveau base de données — même si un bug applicatif oublie le filtre, PostgreSQL bloque. C'est la dernière ligne de défense.

4. Isolation SPINA — Chaîne par Tenant

Principe

Chaque tenant a sa propre chaîne SPINA avec un bloc genesis unique. Les chaînes sont physiquement séparées (fichiers distincts). Les signatures Ed25519 sont liées à la clé du tenant — impossible d'injecter un bloc d'un tenant dans un autre.

Structure fichiers :
/opt/nova/spina/
  ├── tenant-a/
  │   ├── chain.dat        ← chaîne du MSP A
  │   ├── genesis.json     ← bloc #0 unique
  │   └── peers.json       ← pairs du tenant A
  ├── tenant-b/
  │   ├── chain.dat        ← chaîne du MSP B
  │   ├── genesis.json     ← bloc #0 unique (différent de A)
  │   └── peers.json
  └── tenant-c/
      └── ...

API SPINA (port 5110) → filtre par ?tenant=xxx
POST /api/spina/add?tenant=a  → ajoute dans chain.dat du tenant A
GET  /api/spina/blocks?tenant=b → lit UNIQUEMENT la chaîne du tenant B

5. Isolation NOVA — Processus par Tenant

Principe

Chaque tenant a ses propres instances des 7 systèmes NOVA. Chaque processus est isolé par port TCP et répertoire de données. Zéro mémoire partagée entre tenants.

Ports réservés par tenant :
┌──────────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ Tenant   │ SYN  │ CYT  │ HOME │ DERM │ MESH │ ALFA │ SPINA│
├──────────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│ tenant-a │ 5191 │ 5190 │ 5193 │ 5192 │ 5195 │ 5196 │ 5110 │
│ tenant-b │ 5291 │ 5290 │ 5293 │ 5292 │ 5295 │ 5296 │ 5210 │
│ tenant-c │ 5391 │ 5390 │ 5393 │ 5392 │ 5395 │ 5396 │ 5310 │
└──────────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘

Pattern : port = 5{tenant_index}XX
- tenant 0 (a) → 51XX
- tenant 1 (b) → 52XX
- tenant 2 (c) → 53XX
- etc.

Lancement :
nova organism --tenant a --bind 127.0.0.1
  → écoute sur 51XX, data/ dans /opt/nova/tenants/a/

5.5 Isolation ALFA — Conscience par Tenant

Principe

ALFA est le système de conscience de l'organisme. Il observe, apprend, pose des questions, forme des hypothèses. Chaque tenant a sa propre instance ALFA avec sa propre mémoire, ses propres pensées, son propre apprentissage. Les consciences ne se croisent jamais.

Structure ALFA par tenant :
/opt/nova/tenants/{tenant_id}/alfa/
  ├── memory.json       ← mémoire persistante (pensées, hypothèses)
  ├── attention.json    ← focus actuel, curiosité, alertness
  ├── learned/          ← patterns appris (spécifiques au réseau du tenant)
  │   ├── normal_traffic.json
  │   └── attack_patterns.json
  └── dialogue.log      ← conversations avec le greffé

Isolation ALFA :
┌──────────────────────────────────────────────────────────────┐
│ TENANT A — Garage Durand                   TENANT B — Hôpital│
│ ─────────────────────                      ──────────────────│
│                                              │
│ ALFA-A : "Je vois 5 équipements"           ALFA-B : "Je vois 340 équipements"
│ ALFA-A : "Le port 443 est anormal"         ALFA-B : "Le VLAN 12 a un nouveau MAC"
│ ALFA-A apprend le trafic du garage         ALFA-B apprend le trafic de l'hôpital
│                                              │
│        ↑ ZONE ÉTANCHE ↑                      ↑ ZONE ÉTANCHE ↑
│                                              │
│ ALFA-A NE SAIT PAS que l'hôpital existe.                                    │
│ ALFA-B NE SAIT PAS que le garage existe.                                    │
│                                              │
│ Même si les deux tournent sur la même machine physique,                     │
│ leurs consciences sont dans des fichiers séparés,                           │
│ des processus séparés, des ports séparés.                                   │
└──────────────────────────────────────────────────────────────┘

Port ALFA par tenant :
tenant-a → 127.0.0.1:5196
tenant-b → 127.0.0.1:5296
tenant-c → 127.0.0.1:5396

Vérification :
curl http://127.0.0.1:5196/thoughts → pensées du tenant A
curl http://127.0.0.1:5296/thoughts → pensées du tenant B
→ Résultats différents. Aucune intersection possible.

Si un MSP demande à ALFA « Que vois-tu ? », ALFA répond avec les données de SON tenant uniquement. Sa mémoire, ses hypothèses, ses alertes perçues — tout est scopé au tenant. Zéro contamination croisée. Chaque ALFA est une conscience souveraine dans sa capsule.

6. Isolation Cockpit — Domaine par Tenant

┌─────────────────────────────────────────────────────────┐
│ NGINX — Virtual Host Routing                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│ server {                                                 │
│   server_name systel-it.fr *.systel-it.fr;              │
│   → proxy_pass http://127.0.0.1:3003;                    │
│   → set_header X-Tenant-ID "tenant-a";                   │
│ }                                                        │
│                                                         │
│ server {                                                 │
│   server_name cabinet-conseil.fr;                        │
│   → proxy_pass http://127.0.0.1:3003;                    │
│   → set_header X-Tenant-ID "tenant-b";                   │
│ }                                                        │
│                                                         │
│ Le cockpit lit X-Tenant-ID → affiche les données        │
│ UNIQUEMENT de ce tenant.                                 │
│                                                         │
│ Let's Encrypt : certbot --nginx -d systel-it.fr         │
│ → SSL automatique par domaine, renouvellement cron.     │
└─────────────────────────────────────────────────────────┘

7. PWA Offline — Contrôle Sans Internet

┌─────────────────────────────────────────────────────────────┐
│ SCÉNARIO : Internet coupé au Garage Durand                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  14h32 — Coupure fibre                                      │
│  ───────────────────                                        │
│  • Internet down                                             │
│  • NOVA Appliance continue de tourner (locale, rack 4U)     │
│  • Le MSP reçoit une alerte SMS (via backup 4G)            │
│                                                             │
│  14h33 — MSP prend le contrôle                              │
│  ──────────────────────────────                             │
│  • Ouvre NOVA Field sur son téléphone                       │
│  • Se connecte au WiFi local du garage                      │
│  • Voit le Holoscope (données en cache)                     │
│  • Voit les alertes Cytokine (calculées localement)         │
│  • Isole un port suspect → action exécutée immédiatement    │
│  • L'action est mise en file d'attente SPINA                │
│                                                             │
│  16h10 — Retour Internet                                    │
│  ───────────────────────                                    │
│  • SPINA sync : tous les blocs locaux → chaîne principale   │
│  • File d'attente vidée : actions appliquées côté cloud     │
│  • Dashboard MSP mis à jour avec les événements offline     │
│  • Zéro perte de données. Zéro interruption de service.    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8. Plan de Ports Complet

ServicePort publicPort localIsolé par
Forge Next.js443 (nginx)127.0.0.1:3003X-Tenant-ID header
API agents.08.ma443 (nginx)127.0.0.1:8300X-Tenant-ID header
NATSAucun127.0.0.1:4222Préfixe sujet
SPINA APIAucun127.0.0.1:5110?tenant= param
SPINA v2 (eIDAS)Aucun127.0.0.1:5099?tenant= param
GRAFTAucun127.0.0.1:5100?tenant= param
NOVA SYNAPSEAucun127.0.0.1:51XXPort dédié par tenant
NOVA CYTOKINEAucun127.0.0.1:51XXPort dédié par tenant
NOVA HOMEOSTASISAucun127.0.0.1:51XXPort dédié par tenant
NOVA DERMAAucun127.0.0.1:51XXPort dédié par tenant
NOVA MESHAucun127.0.0.1:51XXPort dédié par tenant
NOVA ALFAAucun127.0.0.1:51XXPort dédié par tenant
PostgreSQLAucun127.0.0.1:5432Row-Level Security
RedisAucun127.0.0.1:6379Préfixe clé tenant:{id}:
Node ExporterAucun127.0.0.1:9100Métriques agrégées
Cockpit ServerAucun (ufw deny)127.0.0.1:9090Bloqué externe
CloudPanelAucun (ufw deny)127.0.0.1:8443Bloqué externe

Zéro port public sauf 22 (SSH), 80 (HTTP→443), 443 (HTTPS nginx). Tout le reste est 127.0.0.1.

9. Déploiement d'un Nouveau Tenant — Checklist

1. CRÉER LE TENANT
   ├── UUID généré → tenant_id
   ├── INSERT INTO "Organization" (id, name, slug)
   └── INSERT INTO "Domain" (organizationId, domain)

2. CONFIGURER DNS
   ├── Ajouter A record → 217.160.192.96
   └── certbot --nginx -d {domaine}

3. CONFIGURER NGINX
   ├── Nouveau bloc server { server_name {domaine}; }
   ├── proxy_set_header X-Tenant-ID "{tenant_id}";
   └── nginx -s reload

4. INITIALISER SPINA
   ├── Créer /opt/nova/spina/{tenant_id}/
   ├── Générer bloc genesis (hash SHA256 unique)
   └── Démarrer spina --tenant {tenant_id}

5. DÉMARRER NOVA
   ├── Calculer ports : 5{index}XX
   ├── Créer /opt/nova/tenants/{tenant_id}/
   ├── nova organism --tenant {tenant_id} --bind 127.0.0.1
   └── Vérifier : ss -tlnp | grep 5{index}

6. ACTIVER RLS PostgreSQL
   ├── ALTER TABLE ... ENABLE ROW LEVEL SECURITY
   ├── CREATE POLICY tenant_isolation ...
   └── Vérifier : tentative de lecture croisée → 0 ligne

7. CONFIGURER COCKPIT
   ├── URL cockpit → https://{domaine}/dashboard
   ├── JWT signé avec tenant_id claim
   └── Vérifier : changement ?tenant= dans l'URL → 403

8. ACTIVER PWA OFFLINE
   ├── Servir sw.js + manifest.json
   ├── Cache des assets Holoscope
   └── Vérifier : couper Internet → cockpit toujours accessible

10. Audit Pipeline — Flux Complet

ÉTAPE 0 — Le MSP crée un client
────────────────────────────────
MSP Dashboard → "Nouveau Client" → nom + domaine
→ tenant créé (checklist ci-dessus)
→ URL unique générée : https://{domaine}/audit/{id}

ÉTAPE 1 — Le client exécute le one-liner
─────────────────────────────────────────
$ curl -s https://0data.fr/scan/{id} | bash
→ Phase 0 : détection auto (ARP count → tier S/M/L/XL/XXL)
→ Phase 1 : scan adaptatif (ports, OS, services)
→ Phase 2 : POST /api/scan/submit (avec tenant_id)
→ Phase 3 : redirection → https://{domaine}/audit/{id}/results

ÉTAPE 2 — Holoscope AVANT
─────────────────────────
→ cockpit.html charge les données du scan
→ Affiche le réseau en 3D (grille microscope)
→ Nœuds rouges = vulnérabilités
→ Synapses grises = trafic non surveillé
→ Bouton "Déployer NOVA"

ÉTAPE 3 — Déploiement NOVA
───────────────────────────
→ One-liner avec NOVA_KEY intégrée
→ curl -s https://0data.fr/graft/{tenant_id} | bash
→ Télécharge le symbiote
→ Démarre en mode observe (read-only, zéro risque)
→ Monte les 7 systèmes sur les ports dédiés

ÉTAPE 4 — Holoscope APRÈS
─────────────────────────
→ Même URL, données live
→ Synapses cyan = surveillées par SYNAPSE
→ Alertes or = anomalies détectées par CYTOKINE
→ Arcs verts = corrections HOMEOSTASIS
→ SPINA = X blocs dans la chaîne
→ ALFA = état de conscience
→ Bouton "S'abonner" → Stripe checkout

ÉTAPE 5 — Paiement & Activation
─────────────────────────────────
→ Stripe Checkout (prix selon tier détecté)
→ Webhook checkout.session.completed
→ Service activé dans Organization.services
→ NOVA passe en mode immune (actions réelles)
→ Dashboard MSP mis à jour avec le nouveau client actif

11. Sécurité — Checklist par Tenant

#VérificationCommande
1API bind 127.0.0.1ss -tlnp | grep 5{index} | grep 127.0.0.1
2UFW bloque le portufw status | grep 5{index} → rien
3PostgreSQL RLS actifSELECT * FROM "Service" WHERE "organizationId" != current_setting('app.current_tenant_id')::uuid → 0 ligne
4SPINA chaînes séparéesdiff /opt/nova/spina/A/genesis.json /opt/nova/spina/B/genesis.json → différents
5JWT tenant claimcurl -H "Authorization: Bearer {tokenB}" https://{domaineA}/api/agents → 403
6NATS sujet isolénats sub "tenant.A.>" ne reçoit pas les messages de tenant.B.>
7Redis clé isoléeredis-cli KEYS "tenant:A:*"redis-cli KEYS "tenant:B:*"
8Logs séparésls /var/log/nova/ → un fichier par tenant, permissions 640
9SSL par domaineopenssl s_client -connect {domaine}:443 → certificat valide
10PWA offlineCouper Internet → cockpit accessible, actions en file d'attente