Une base gérée promet des sauvegardes, une restauration à la minute et des copies instantanées. Cet article met ces trois promesses à l'épreuve sur une base PostgreSQL 18 de la plus petite taille (1 Go, 0,020 CAD l'heure), remplie avec 5 millions de lignes, puis vérifie chaque copie par des comptes de lignes et des sommes de contrôle. Le test a été fait le 3 novembre 2026 ; les heures sont celles de Montréal et seuls les mots de passe sont masqués.
La création de la base, la connexion par le réseau privé et le passage à la taille supérieure sont déjà décrits dans Bases de données gérées à Montréal, en démonstration. Ici, on chronomètre ce qui protège les données.
1. Le banc d'essai
Une base PostgreSQL de 1 Go commandée par l'API à 12:06:09, active à 12:12:43. La première sauvegarde complète était faite à 12:12:49 et son essai de restauration réussi à 12:13:14 : la base est protégée avant même la première écriture.
Le client tournait sur une machine à Montréal, hors du réseau privé de la base. L'accès public a donc été ouvert, limité à ses deux adresses (IPv4 et IPv6). Il est en place en une trentaine de secondes :
curl -s -X PUT https://api.ffxf.net/v1/databases/34/public-access \
-H "Authorization: Bearer $FFXF_TOKEN" -H "Content-Type: application/json" \
-d '{"enabled": true, "sources": ["203.0.113.10", "2001:db8::10"]}'
Pour une application hébergée chez FFxF, préférez le réseau privé : rien n'est exposé.
2. TLS obligatoire, vérifié
La chaîne de connexion de GET /v1/databases/34/credentials se termine par
sslmode=verify-full&sslrootcert=system. Le client vérifie le certificat
et le nom du serveur :
$ psql "$DB" -c "SELECT ssl, version AS tls, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
ssl | tls | cipher
-----+---------+------------------------
t | TLSv1.3 | TLS_AES_256_GCM_SHA384
Sans chiffrement, le serveur refuse la connexion :
$ psql "postgresql://ffxfadmin:••••@dsghpaczji.db.ffxf.cloud:28772/defaultdb?sslmode=disable"
psql: error: … FATAL: no pg_hba.conf entry for host "2602:f3a4:c0:ffee::5", user "ffxfadmin", database "defaultdb", no encryption
Un piège rencontré en chemin : l'image Docker officielle postgres:18, utilisée
ici comme client, ne contient pas les autorités de certification du système. Avec
sslrootcert=system, la connexion échoue sur
SSL error: certificate verify failed, alors que le certificat Let's Encrypt
est valide (openssl s_client répond Verify return code: 0 (ok)).
Montez le magasin de l'hôte dans le conteneur, ou installez
ca-certificates :
docker run --rm -it -e DB="$DB" -v /etc/ssl/certs:/etc/ssl/certs:ro postgres:18 psql "$DB"
3. Un jeu de données de taille connue
pgbench, livré avec PostgreSQL, crée quatre tables bancaires. Au facteur 50 :
5 000 000 de comptes, 500 guichets et 50 agences.
$ pgbench -i -s 50 "$DB"
5000000 of 5000000 tuples (100%) of pgbench_accounts done (elapsed 7.03 s, remaining 0.00 s)
done in 12.78 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 7.17 s, vacuum 0.90 s, primary keys 4.70 s).
La base pèse alors 755 Mo. Une minute de transactions à 4 clients ajoute de l'historique : 40 223 transactions, 671,8 par seconde, 5,95 ms de latence moyenne, réseau compris. Les dernières sont écrites à 12:16:02.
Pour comparer les copies, une requête calcule, par table, le nombre de lignes, la somme
des soldes et une empreinte : la somme des hashtext() de chaque ligne
entière. Une seule valeur modifiée change l'empreinte.
SELECT 'accounts' t, count(*) n, sum(abalance) s, sum(hashtext(a::text)::bigint) h FROM pgbench_accounts a
UNION ALL SELECT 'branches', count(*), sum(bbalance), sum(hashtext(b::text)::bigint) FROM pgbench_branches b
UNION ALL SELECT 'tellers', count(*), sum(tbalance), sum(hashtext(x::text)::bigint) FROM pgbench_tellers x
UNION ALL SELECT 'history', count(*), sum(delta), sum(hashtext(y::text)::bigint) FROM pgbench_history y;
État de référence, à 12:16:09 :
t | n | s | h
----------+---------+---------+---------------
accounts | 5000000 | -383125 | 1430067750301
branches | 50 | -383125 | -12480793191
tellers | 500 | -383125 | -86711664516
history | 40223 | -383125 | 80768475562
4. Une sauvegarde à la demande
Les sauvegardes sont automatiques. Avant une opération risquée, on peut en lancer une tout de suite :
curl -s -X POST https://api.ffxf.net/v1/databases/34/backups \
-H "Authorization: Bearer $FFXF_TOKEN" -H "Idempotency-Key: $(uuidgen)"
Demandée à 12:16:15, elle démarre à 12:16:22 et se termine à 12:16:52 : 30 secondes.
C'est une différentielle, prise par rapport à la complète de 12:12:40, et elle pèse
40,5 Mo pour 755 Mo de données (les tables de pgbench se compressent bien).
Plus tard, la première complète d'une base de même contenu a pris 34 secondes pour
43,2 Mo.
{ "label": "20261103-171240F_20261103-171622D", "kind": "diff",
"started_at": "2026-11-03T17:16:22Z", "finished_at": "2026-11-03T17:16:52Z", "size_bytes": 40461008 }
5. L'erreur, puis la restauration à la minute
À 12:18:31, une mise à jour part sans WHERE :
UPDATE pgbench_accounts SET abalance = 0;
UPDATE 5000000
Time: 49896.622 ms (00:49.897)
La restauration se fait à partir de la sauvegarde complète et du journal des transactions
archivé en continu. N'importe quel instant de restore_window convient. À
12:19:39, la fenêtre allait jusqu'à 12:17:51 : le journal arrive hors site avec une ou
deux minutes de retard (43 secondes lors d'une autre lecture). L'instant choisi est
12:17:30, après les dernières transactions et avant l'erreur, donné en UTC :
curl -s -X POST https://api.ffxf.net/v1/databases/34/restore \
-H "Authorization: Bearer $FFXF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"at": "2026-11-03T17:17:30Z", "name": "banque-avant-erreur"}'
| Heure | Restauration |
|---|---|
| 12:19:45 | Demandée ; statut restoring |
| 12:25:16 | Moteur démarré sur la nouvelle VM |
| 12:25:37 | Données restaurées (restored_from.done) |
| 12:25:53 | Base active, 6 min 8 s après la demande |
| 12:26:23 | Sa propre première sauvegarde complète |
| 12:30:02 | Certificat en place |
L'essentiel des six minutes sert à préparer la machine. Les empreintes de la base restaurée sont identiques, chiffre pour chiffre, à l'état de référence, et la dernière ligne d'historique date de 12:16:02.576 :
accounts | 5000000 | -383125 | 1430067750301
branches | 50 | -383125 | -12480793191
tellers | 500 | -383125 | -86711664516
history | 40223 | -383125 | 80768475562
La base restaurée garde l'utilisateur et le mot de passe de la source ; seul l'hôte change. La source, elle, n'est jamais écrasée : ses comptes sont toujours à zéro, prêts à être examinés.
6. Le fork : une copie en 35 secondes
Un fork copie la base telle qu'elle est maintenant, ici avec l'erreur :
curl -s -X POST https://api.ffxf.net/v1/databases/34/forks \
-H "Authorization: Bearer $FFXF_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "banque-preprod"}'
Demandé à 12:20:04, le fork était lié à ses données à 12:20:39, 35 secondes plus tard
pour 755 Mo. Il est devenu actif à 12:29:30 : 9 min 26 s, demandé 19 secondes après la
restauration et préparé en même temps qu'elle. Ses empreintes sont celles de la source après l'erreur
(accounts | 5000000 | 0 | 1464243819646, les trois autres tables inchangées).
Une écriture sur le fork ne touche pas la source :
-- sur le fork
UPDATE pgbench_accounts SET abalance = 1 WHERE aid <= 1000;
UPDATE 1000
-- sur la source
SELECT count(*) FILTER (WHERE abalance <> 0), sum(abalance) FROM pgbench_accounts;
0|0
Tant qu'il est lié, le fork n'a pas de sauvegardes (GET …/backups renvoie une
liste vide) et la source ne peut pas être supprimée :
{"error":{"code":"action_conflict","message":"Forks of this database still depend on it: make them independent or delete them first."}}
POST /v1/databases/36/detach lui donne sa propre copie. Une sonde interrogeait
le fork chaque seconde : dernière réponse à 12:34:08, connexions refusées ensuite,
première réponse revenue à 12:34:35. Environ 27 secondes de coupure ; les 1 000 lignes
modifiées sont toujours là. D'après la documentation, le fork a dès lors ses propres sauvegardes.
7. pgvector et PostGIS
Les extensions de la liste de GET /v1/databases/options s'activent par
l'API, base logique par base logique. La réponse est immédiate :
curl -s -X POST https://api.ffxf.net/v1/databases/35/databases/defaultdb/extensions \
-H "Authorization: Bearer $FFXF_TOKEN" -H "Content-Type: application/json" \
-d '{"extension": "vector"}'
{"data":{"name":"defaultdb","extensions":["vector"]}}
Avec postgis ajoutée de la même façon, la base contenait
vector 0.8.7, postgis 3.7.0 et pg_stat_statements 1.12,
présente d'office. Un index HNSW et un calcul de distance fonctionnent tels quels :
CREATE TABLE docs (id int PRIMARY KEY, embedding vector(3));
INSERT INTO docs VALUES (1, '[1,0,0]'), (2, '[0.9,0.1,0]'), (3, '[0,0,1]');
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
SELECT id, round((embedding <=> '[1,0,0]')::numeric, 4) AS distance
FROM docs ORDER BY embedding <=> '[1,0,0]' LIMIT 2;
id | distance
----+----------
1 | 0.0000
2 | 0.0061
SELECT round(ST_Distance('SRID=4326;POINT(-73.5674 45.5019)'::geography,
'SRID=4326;POINT(-71.2080 46.8139)'::geography)::numeric / 1000, 1) AS montreal_quebec_km;
montreal_quebec_km
--------------------
233.4
L'administrateur ffxfadmin n'est pas superutilisateur. Une extension dite de
confiance comme hstore se crée aussi en SQL ; dblink et
file_fdw sont refusées (Must be superuser to create this
extension).
8. Les limites, telles que mesurées
- Rétention de 7 jours. Au-delà, plus de restauration possible. Pour
garder une copie plus longtemps, faites votre propre export : un
pg_dump -Fcde cette base de 755 Mo a pris 7,7 secondes et produit un fichier de 15 Mo. - Pas de téléchargement des sauvegardes. L'API les liste et les restaure dans une nouvelle base ; elle ne les livre pas en fichier.
- Une restauration rend toute la base. Pour récupérer une seule table,
restaurez, puis copiez la table avec
pg_dump -t, et supprimez la base restaurée. - Les dernières minutes. La fenêtre de restauration suit le présent avec une à deux minutes de retard.
- L'historique ne suit pas. La base restaurée commence sa propre fenêtre à sa première sauvegarde (12:26:23) ; celle de la source reste accessible tant que la source existe, puis 7 jours après sa suppression.
- Chaque copie est une base facturée. Restauration et fork coûtent le tarif de leur taille, à l'heure, jusqu'à leur suppression.
- 1 Go, c'est petit. 25 connexions au plus et
shared_buffersde 241 Mo : prévoyez un pool de connexions côté application.
9. Bilan
| Opération | Durée mesurée | Vérification |
|---|---|---|
| Création, 1 Go | 6 min 34 s | Sauvegarde et essai de restauration faits avant l'activation |
| Chargement de 5 M de lignes | 12,8 s | 755 Mo |
| Sauvegarde à la demande | 30 s (40,5 Mo) | Différentielle, listée dans …/backups |
| Restauration à 12:17:30 | 6 min 8 s | 4 empreintes identiques à l'état de référence |
| Fork | 35 s pour les données, 9 min 26 s pour l'activation | Empreintes identiques à la source |
| Détachement du fork | 27 s de coupure | Données conservées |
| Suppression des trois bases | 33 s | not_found à la relecture |
Les trois bases ont vécu entre 15 et 29 minutes. À l'heure, le test entier coûte environ 0,06 CAD. Les appels utilisés sont tous décrits dans la documentation des bases de données.