← Retour au blog

Créer un SaaS B2B souverain avec Lampion

Guide · Mis à jour le · 11 min de lecture · Équipe Lampion

Vendre un logiciel à des entreprises françaises, c’est passer par un questionnaire sécurité avant de passer par un bon de commande. Deux décisions y répondent presque entièrement : où tourne votre infrastructure, et comment vous séparez les données de vos clients.

Ce guide prend les deux dans l’ordre, avec les commandes qui vont avec. Comptez une heure pour arriver à une base PostgreSQL 17 hébergée en France, un client qui ne peut techniquement pas lire les données d’un autre, et une base de test éphémère à chaque pull request.

Contexte

Ce que l’acheteur va demander

Un cycle de vente B2B s’arrête rarement sur la fonctionnalité. Il s’arrête sur l’annexe technique du contrat, et cette annexe se prépare dans l’infrastructure.

Votre interlocuteur métier est convaincu, puis le dossier passe au responsable de la sécurité et au juriste. Arrivent alors les mêmes questions : où sont nos données, qui peut les lire, comment garantissez-vous qu’un autre client n’y accède pas, et comment les récupérons-nous si nous partons. Aucune ne se répond après coup.

Localisation
Où tournent vos serveurs, et quelle société les opère. La réponse attendue est une région et un nom, pas un continent.
Cloisonnement
Comment votre application empêche techniquement un client de lire les lignes d’un autre.
Accès interne
Qui, chez vous, peut lire une donnée client en clair.
Réversibilité
Un export exploitable, dans un format standard, le jour où le client s’en va.

Les deux premières questions se règlent dans les étapes 01 et 03. Les deux autres tombent presque toutes seules ensuite.

Étape 01

Choisir où tout tourne

La base de données n’est qu’une brique. Si l’API, les fichiers ou les logs partent ailleurs, la réponse « nos données sont en France » ne tient pas cinq minutes en audit.

C’est l’erreur la plus fréquente : une base hébergée en France, et le reste du service chez un fournisseur non européen. Or les données personnelles ne restent pas dans la base. Elles passent par l’API, s’écrivent dans les logs, se posent dans un cache, se retrouvent dans un e-mail transactionnel et dans les sauvegardes de tout ce qui précède. Chaque brique hébergée ailleurs est une copie ailleurs.

Où passent les données d’un client API · backend tout, en mémoire Base de données la source de vérité Stockage fichiers pièces jointes, exports E-mails noms, adresses, objets Journaux · métriques identifiants, requêtes Sauvegardes une copie de tout Une seule brique hébergée hors d’Europe suffit à rendre la réponse au questionnaire fausse.
Fig. 1 — Les données personnelles ne vivent pas que dans la base. Chaque brique en manipule une copie : l’hébergement se raisonne sur la ligne entière, pas sur le seul Postgres.

Pourquoi l’opérateur compte autant que la région

La loi qui s’applique
Un centre de données en France opéré par une filiale d’un groupe non européen reste rattaché au droit de sa maison mère. La question de votre acheteur porte sur la société, pas sur le bâtiment.
La chaîne complète
Vous êtes responsable de vos sous-traitants. Chaque fournisseur de la ligne précédente doit être nommé dans le contrat que vous signerez avec votre client.
La réversibilité
Des formats standards — Postgres, S3, conteneurs — vous laissent changer d’avis. C’est aussi ce qui rassure un acheteur sur votre propre pérennité.
Le coût réel
Les fournisseurs français facturent en euros, sans frais de sortie surprise sur le transfert. Sur une petite infrastructure, l’écart est rarement à leur défaveur.

Où faire tourner quoi

La liste ci-dessous n’est pas exhaustive et n’est pas un classement : ce sont des fournisseurs français ou européens couramment utilisés pour chaque brique. Ce qui compte, ce sont les critères de la dernière colonne.

BriqueOptions françaises ou européennesCe qu’il faut vérifier
Base de donnéesLampion (région fr-par-1), ou un Postgres managé chez Scaleway, OVHcloud, Clever CloudLa région, et surtout où partent les sauvegardes — c’est souvent là que la donnée sort
API · backendConteneurs ou machines chez Scaleway, OVHcloud, Outscale ; plateforme applicative Clever CloudLe pays d’exécution, mais aussi celui du plan de contrôle et du registre d’images
Stockage fichiersStockage objet compatible S3 chez Scaleway ou OVHcloudLa région du bucket, le chiffrement, et toute réplication automatique hors région
E-mails transactionnelsFournisseurs européens ; à défaut, réduire le contenu au strict minimumCe que contient l’e-mail : un objet de message suffit parfois à révéler une donnée de santé
Journaux · métriquesCollecte et stockage en Europe, rétention courteLes logs applicatifs contiennent presque toujours des identifiants clients
CDN · edgePoints de présence européens, ou pas de CDN devant les routes authentifiéesUn cache est une copie, et il vit là où se trouve le point de présence
Les certifications bougent, les offres aussi : ne recopiez pas cette table dans un contrat. Les quatre critères qui tiennent dans le temps sont le siège social et l’actionnariat de l’opérateur, le lieu d’exécution réel, la chaîne de sous-traitance nommée, et les certifications vérifiées à la date où vous répondez.
Étape 02

Créer la base en France

Trois commandes. La région se choisit à la création du projet et les données n’en sortent pas.

bashterminal
# Installer le CLI et poser le jeton
$ pip install lampion-cli
$ export LAMPION_TOKEN=lmp_live_xxxxxxxxxxxxxxxx

# Créer le projet dans la région de Paris
$ lampion projects create saas-demo --region fr-par-1
Project created: 4f9c21ab8d3e47b0a6c95e1d2b3f7a08 (saas-demo, fr-par-1)
Endpoint: ep-4f9c21ab8d3e
Connection: postgresql://cloud_admin:••••@db.lampion.cloud:5432/ep-4f9c21ab8d3e.postgres?sslmode=require

# Cette chaîne est celle du propriétaire : elle sert aux migrations
$ export ADMIN_DATABASE_URL="postgresql://cloud_admin:••••@db.lampion.cloud:5432/ep-4f9c21ab8d3e.postgres?sslmode=require"

# Vérifier la version et la localisation
$ psql "$ADMIN_DATABASE_URL" -tAc "SELECT version()"
PostgreSQL 17.…
$ lampion residency 4f9c21ab8d3e47b0a6c95e1d2b3f7a08
Paris 1 — Scaleway DC2 — Vitry-sur-Seine
Country: France | Host certifications (Scaleway): ISO 27001, HDS, SOC 2 Type II, PCI DSS
Lampion certifications: none yet (host certifications do not cover Lampion)

Deux détails dans cette URL : le nom de database porte l’identifiant du compute, c’est ce qui permet au proxy de router la connexion ; et sslmode=require n’est pas négociable. Notez le nom de la variable — ADMIN_DATABASE_URL, pas DATABASE_URL : l’application aura son propre rôle à l’étape suivante. Les sorties sont abrégées, et les mots de passe masqués, dans tout l’article.

Prérequis

Un compte
Le plan Free suffit : 3 projets, 3 branches, un compute de 0,25 CU et 512 Mo de stockage, sans carte bancaire.
Une API key
À générer dans Settings › API Keys de la console. Préfixe lmp_live_, valable 90 jours.
Python 3.10+
Le CLI est un paquet PyPI. Tout ce qu’il fait existe aussi en REST sur https://api.lampion.cloud/v1.
Étape 03

Isoler chaque client

Deux niveaux de cloisonnement. Entre projets, Lampion sépare le stockage : chaque projet est un tenant, avec son propre historique. Dans un projet, une colonne tenant_id et une policy PostgreSQL font refuser la ligne par le moteur.

Sur Lampion, chaque projet est un tenant du moteur de stockage : ses pages, son WAL et ses fichiers dans le stockage objet sont rangés sous son propre identifiant. Depuis le 10 octobre 2026, le serveur de pages vérifie à chaque lecture un jeton propre au projet : un compute ne lit que les pages de son tenant. Chaque projet a aussi son propre historique — restaurer un projet à un instant donné ne touche aucun autre projet — et supprimer un projet supprime son tenant, sur le serveur de pages, le safekeeper et le stockage objet.

Ce qu’un projet par client apporte

Stockage séparé
Un tenant par projet : pages, WAL et fichiers du stockage objet rangés à part. La lecture des pages est vérifiée par un jeton propre au projet : un compute ne peut pas lire les pages d’un autre client.
Historique propre
Chaque projet garde son historique pendant la durée de son plan — 12 heures sur Free, 30 jours sur Startup, 90 jours sur Souverain — et se restaure à la seconde, sans toucher aux autres clients.
Effacement par projet
Supprimer le projet supprime son tenant, avec ses données et son historique, sans toucher aux autres clients. C’est le point de départ le plus simple d’une demande d’effacement ou d’une fin de contrat.
Compute à part
Chaque projet a son propre compute, qui se suspend quand le client est inactif : un client qui dort ne coûte que son stockage.

Le prix : une migration et une connection string par client. Startup autorise jusqu’à 1 000 projets, Souverain bien davantage ; au-delà de quelques dizaines de clients, le schéma partagé reste le plus simple à exploiter.

Vous avez donc deux façons de séparer vos clients. Un projet par client : le cloisonnement va jusqu’au stockage, et chaque client a son historique, sa restauration et son effacement. Un schéma partagé avec row-level security : tous les clients dans un même projet, filtrés par le moteur PostgreSQL. Les deux cohabitent très bien — vos grands comptes dans leur propre projet, les autres dans un projet partagé —, et la suite de cette étape met en place le second, qui demande le plus de rigueur.

01 Schéma partagé une colonne tenant_id sur chaque table 1 projet · 1 tenant · N clients public.orders tenant_1 tenant_2 tenant_3 tenant_1 tenant_2 + Le moins cher, une seule migration − Une policy oubliée et les lignes fuient 02 Database par client une database = un client, même projet 1 projet · 1 tenant · N databases db_1 · orders db_2 · orders db_3 · orders + Pas de policy à maintenir − Historique et restauration communs 03 Projet par client un projet = un tenant = un client N projets · N tenants projet 1 1 tenant projet 2 2 tenant projet 3 3 tenant + Stockage, historique, compute séparés − N migrations, N connection strings
Fig. 2 — De gauche à droite, l’isolation augmente et le coût d’exploitation aussi. Seul le troisième modèle sépare aussi le stockage et l’historique : une database par client partage toujours le tenant, donc l’historique, de son projet.
sqlschema.sql
-- Toute table portant des données client a un tenant_id.
CREATE TABLE tenants (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug       text UNIQUE NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id  uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  amount_cts integer NOT NULL CHECK (amount_cts >= 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX orders_tenant_created_idx
  ON orders (tenant_id, created_at DESC);

memberships est la table qui relie un utilisateur à son client : c’est elle qui permet au serveur de décider quel tenant_id poser. Elle porte aussi une policy, comme toute table contenant des données client — la row-level security ne traverse pas les jointures.

sqlpolicies.sql
-- Le moteur filtre, plus seulement le code applicatif.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- FORCE : la policy s'applique aussi au propriétaire de la table.
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
  USING      (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);

-- Le rôle applicatif n'est jamais le propriétaire.
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
GRANT SELECT ON tenants TO app_user;

NULLIF évite un piège : après un premier set_config local suivi d’un COMMIT, la valeur peut revenir à la chaîne vide, et ''::uuid lève une erreur au lieu de filtrer. Avec NULLIF, le cas non renseigné ne rend aucune ligne — l’échec est fermé.

bashterminal
# Le rôle de l'application, puis sa connection string
$ export APP_PASSWORD="$(openssl rand -hex 24)"
$ lampion roles create 4f9c21ab8d3e47b0a6c95e1d2b3f7a08 ep-4f9c21ab8d3e app_user --password "$APP_PASSWORD"
Role 'app_user' created.
$ export DATABASE_URL="postgresql://app_user:[email protected]:5432/ep-4f9c21ab8d3e.postgres?sslmode=require"

# Appliquer le schéma et les policies
$ psql "$ADMIN_DATABASE_URL" -v ON_ERROR_STOP=1 -f schema.sql -f policies.sql

# Une ligne de test, écrite par le propriétaire
$ psql "$ADMIN_DATABASE_URL" -c "INSERT INTO tenants (slug) VALUES ('acme')"
$ psql "$ADMIN_DATABASE_URL" -c "INSERT INTO orders (tenant_id, amount_cts) SELECT id, 1000 FROM tenants"

# Relue par l'application, sans tenant posé : la policy ne rend rien
$ psql "$DATABASE_URL" -c "SELECT count(*) FROM orders"
 count 
-------
     0
Le piège classique

La connection string que Lampion vous remet utilise cloud_admin, le propriétaire des objets. Si votre application se connecte avec ce rôle, FORCE ROW LEVEL SECURITY est votre seule protection et un oubli suffit à tout ouvrir. Gardez deux variables distinctes : ADMIN_DATABASE_URL pour les migrations, DATABASE_URL pour l’application. La liste des personnes qui détiennent la première est votre vraie surface d’accès interne.

Étape 04

Brancher l’application

La policy lit app.tenant_id. Il reste à écrire ce paramètre au bon endroit : dans la transaction, parce que la connexion, elle, passe d’un client à l’autre.

01 Requête HTTP jeton de session → tenant résolu 02 Pool applicatif pool.connect() connexion réutilisée 03 BEGIN set_config('app.tenant_id', $1, true) ← local 04 Policy RLS USING (tenant_id = NULLIF(current_setting…)) 05 COMMIT paramètre annulé connexion rendue au pool La connection string Lampion mène au compute à travers un proxy, sans pooler : le pool, c’est celui de votre application. Un SET sans LOCAL survivrait à la transaction, et la requête suivante — peut-être celle d’un autre client — en hériterait.
Fig. 3 — Le paramètre app.tenant_id ne vit que le temps d’une transaction : c’est ce qui rend sûre la réutilisation d’une connexion d’un client à l’autre, par le pool de votre application comme par n’importe quel pooler en transaction mode.
typescriptdb.ts — node-postgres
// Une seule porte vers la base : le contexte ne peut pas être oublié.
import pg from 'pg'

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: { rejectUnauthorized: true },
})

export async function withTenant<T>(
  tenantId: string,
  fn: (c: pg.PoolClient) => Promise<T>,
): Promise<T> {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')

    // Local : annulé au COMMIT, avant que le pool ne prête la connexion à un autre client.
    await client.query('SELECT set_config($1, $2, true)',
                       ['app.tenant_id', tenantId])

    const out = await fn(client)
    await client.query('COMMIT')
    return out
  } catch (err) {
    await client.query('ROLLBACK')
    throw err
  } finally {
    client.release()
  }
}

Les trois règles

SET LOCAL
Troisième argument à true, toujours dans un BEGIN. Le paramètre meurt au COMMIT : c’est la seule forme sûre dès qu’une connexion sert plusieurs clients, dans le pool de l’application comme derrière un PgBouncer en transaction mode.
Rôle dédié
L’application se connecte avec app_user : non propriétaire, sans SUPERUSER ni BYPASSRLS.
Source de vérité
Le tenant vient de la session vérifiée côté serveur, résolu via memberships — jamais d’un paramètre de requête ou d’un en-tête.

Gardez ce pool petit et ses connexions longues : le proxy de Lampion accepte au plus 50 connexions simultanées par endpoint, et 20 nouvelles connexions par minute depuis une même adresse IP — moins que le max_connections du compute, de 100 à 0,25 CU à 3 357 à 8 CU. Une connexion par requête HTTP bute vite sur la seconde limite.

Étape 05

Une base par pull request

Une branch Lampion est un fork copy-on-write de la production au LSN courant : les vraies données, en quelques secondes, sans copie physique et sans sortir de la région.

main — production branch pr-482 la migration testée sur les données réelles branch pr-495 seeds rejoués — trois clients de démonstration Stockage du projet, partagé par ses branches — aucune copie physique des données
Fig. 4 — Chaque branch part du LSN (Log Sequence Number) courant de main et partage son stockage. Le job de nettoyage la supprime à la fermeture de la pull request ; son TTL la supprime de toute façon si ce job ne passe pas.
bashterminal
# Une branch forkée depuis main, supprimée d’office au bout de 72 h
$ lampion branches create 4f9c21ab8d3e47b0a6c95e1d2b3f7a08 pr-482 --parent 4f9c21ab8d3e47b0a6c95e1d2b3f7a08 --ttl 72
Branch created: 9b2e7c41d0a54f3e8c6a1b7d2e9f0c35 (pr-482)

Ce que ça débloque

Preview par PR
Chaque pull request a sa base. Les revues portent sur le comportement réel, plus sur une intention de migration.
Seeds
Des scripts SQL ordonnés, rejoués à la création et au reset de chaque branch : un par projet sur Free, jusqu’à dix sur Startup.
TTL
Une durée de vie d’une heure à trente jours (--ttl, en heures) supprime la branch toute seule : le filet si le nettoyage de fin de pull request échoue.
Branch protégée
main peut être marquée protégée : ni reset, ni suppression accidentelle depuis un script d’intégration continue.
yaml.github/workflows/preview.yml
# Une base éphémère par pull request : supprimée à la fermeture, TTL en filet
name: preview
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

# Un seul run à la fois par pull request : deux pushes rapprochés ne créent pas deux branches.
concurrency: preview-${{ github.event.number }}

env:
  LAMPION_TOKEN: ${{ secrets.LAMPION_TOKEN }}
  PROJECT_ID: 4f9c21ab8d3e47b0a6c95e1d2b3f7a08
  BRANCH: pr-${{ github.event.number }}

jobs:
  test:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install lampion-cli

      - name: Créer la branch au premier passage, la remettre à neuf ensuite
        run: |
          BRANCH_ID=$(lampion branches list "$PROJECT_ID" --json \
            | jq -r --arg b "$BRANCH" '.[] | select(.name == $b) | .id')
          if [ -z "$BRANCH_ID" ]; then
            # TTL de 72 h : le filet si le job cleanup ne passe pas
            lampion branches create "$PROJECT_ID" "$BRANCH" \
              --parent "$PROJECT_ID" --ttl 72 --json
          else
            # Push suivant : repartir de l'état courant de main
            lampion branches reset "$PROJECT_ID" "$BRANCH_ID" --yes
          fi

      - name: Récupérer son URL
        run: |
          URL=$(lampion endpoints list "$PROJECT_ID" --json \
            | jq -r --arg b "$BRANCH" '.[] | select(.name == $b) | .connection_string')
          echo "::add-mask::$URL"
          echo "ADMIN_DATABASE_URL=$URL" >> "$GITHUB_ENV"

      - name: Migrations et tests
        run: |
          npm ci
          npm run migrate   # les migrations de la PR, sur la copie de la production
          npm test

  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install lampion-cli

      - name: Supprimer la branch
        run: |
          BRANCH_ID=$(lampion branches list "$PROJECT_ID" --json \
            | jq -r --arg b "$BRANCH" '.[] | select(.name == $b) | .id')
          # Vide si le TTL l'a déjà supprimée : rien à faire
          if [ -n "$BRANCH_ID" ]; then
            lampion branches delete "$PROJECT_ID" "$BRANCH_ID"
          fi

Aucun frais par branche, quel que soit le plan — seuls le compute consommé et le stockage sont facturés. Une branch inactive se suspend après cinq minutes. Pour aller plus loin et faire tourner les tests sur des données masquées, il y a un article dédié.

Tester sur des données de production masquées, à chaque pull request →

Étape 06

Répondre au questionnaire

Les questions de l’ouverture, et celles qui les suivent toujours, attendent des réponses écrites. Voici où trouver chacune, et ce qu’on peut réellement affirmer.

Localisation
Région fr-par-1, vérifiable par lampion residency, et attestée par un rapport de résidence PDF horodaté. Données actives, journaux WAL et snapshots restent dans la région choisie.
Opérateur
Infrastructure Scaleway, société de droit français du groupe Iliad, datacenters de la région parisienne. Le détail par couche est sur la page DPA ; la liste des sous-traitants, avec ceux qui voient réellement vos données, est dans la console.
Le reste de la pile
La même question se pose pour votre API, vos fichiers et vos logs. C’est la table de l’étape 01, et c’est la partie que votre acheteur regardera après la base.
Cloisonnement
Entre projets : un tenant de stockage par projet, lecture des pages vérifiée par jeton — d’où un projet dédié pour les clients qui l’exigent. Dans un projet : RLS activée et forcée, rôle applicatif non propriétaire, et un test d’isolation rejoué à chaque commit sur une branch forkée de la production.
Accès interne
RBAC owner · admin · developer · viewer · analyst, journal d’audit des actions de la console, et masquage des données personnelles pour le rôle analyste.
Réversibilité
lampion dump exporte en SQL, format custom ou CSV (--format sql|custom|csv ; le CSV se fait table par table). Restaurez-le ailleurs une fois et notez la date : un export jamais restauré n’est pas une réversibilité. Pour un client dans son propre projet, l’effacement en fin de contrat commence par la suppression du projet, qui supprime son tenant. N’écrivez pas de délai de purge des copies de sauvegarde sans l’avoir fait confirmer par écrit.
Pièces à joindre
L’onglet Conformité de la console les rassemble : un questionnaire sécurité pré-rempli (export Markdown, CSV ou JSON), l’acceptation du DPA en ligne avec son attestation PDF, la liste des sous-traitants et le rapport de résidence en PDF.
bashterminal
# Le questionnaire pré-rempli, à donner à votre assistant IA
$ lampion compliance questionnaire --format md -o questionnaire.md

# Accepter le DPA au nom de la société, puis télécharger l’attestation
$ lampion compliance dpa-accept --company "Acme SAS" --signer "Camille Martin" --title "CEO"
$ lampion compliance dpa-certificate --pdf dpa-attestation.pdf

# Le rapport de résidence et la liste des sous-traitants
$ lampion compliance residency --pdf residence.pdf
$ lampion compliance subprocessors

Les mêmes documents, en ligne de commande. Chaque réponse du questionnaire porte un statut — oui, partiel, non, prévu — que l’assistant ne doit pas arrondir. Seul l’owner de l’organisation peut accepter le DPA. Les documents sont en français par défaut ; --locale en donne la version anglaise.

Ce qu’on ne promet pas

Une certification ne se déclare pas, elle s’obtient — et elle appartient à celui qui la détient, pas à celui qui la revend. Ce que vous pouvez affirmer sans risque : la région, les sociétés de la chaîne de sous-traitance, les certifications que chacune détient à la date où vous répondez, le chiffrement du transport, le cloisonnement du stockage par projet et le cloisonnement applicatif, et vos procédures d’effacement et d’export. Pour le reste, renvoyez à la page DPA. Un questionnaire rempli avec exactitude passe mieux qu’un questionnaire rempli avec optimisme.

Checklist

Avant d’ouvrir les inscriptions

Neuf vérifications, dans l’ordre où elles se cassent en production.

  1. 01Chaque brique de la pile — API, fichiers, e-mails, journaux — est hébergée dans l’Union européenne, et vous savez nommer l’opérateur de chacune.
  2. 02Les clients qui exigent un cloisonnement du stockage, une restauration ou un effacement à part ont leur propre projet ; les autres partagent un projet.
  3. 03Dans un projet partagé, toute table portant des données client a un tenant_id NOT NULL et une clé étrangère vers tenants.
  4. 04ENABLE et FORCE ROW LEVEL SECURITY sont actifs sur ces tables, avec un USING et un WITH CHECK par policy.
  5. 05Les policies comparent avec NULLIF(current_setting(…), ''), pour que le cas non renseigné refuse au lieu de lever.
  6. 06L’application se connecte avec app_user ; ADMIN_DATABASE_URL n’est jamais déployée avec le code.
  7. 07Le tenant_id est posé par set_config(…, true) dans la transaction, jamais par un SET global.
  8. 08Un test d’isolation tourne à chaque commit, sur une branch forkée de la production.
  9. 09Un export a été restauré ailleurs au moins une fois, et le DPA est accepté, son attestation PDF prête à joindre à une réponse d’appel d’offres.

Votre premier client,
hébergé en France.

Free : 3 projets, 3 branches, 0,25 CU, 512 Mo de stockage — sans carte bancaire. Données hébergées en France.