Persistance & stores
Le kit persiste l'état opérationnel de votre intégration : les installations, les secrets (chiffrés), les curseurs de synchronisation, l'idempotence des appels entrants, l'anti-rejeu des webhooks, et les journaux bornés (webhooks, dead-letter, audit). Ce n'est pas une base métier : la volumétrie est faible et les journaux sont purgés par rétention. Vous n'écrivez aucun SQL.
Depuis le kit v2, cette persistance passe par un port (IntegrationStore) avec deux adapters officiels au comportement strictement identique :
| Store | Pour qui | Driver à installer |
|---|---|---|
| SQLite (défaut) | déploiement avec disque persistant, zéro configuration | better-sqlite3 |
| PostgreSQL | brancher votre base existante (conteneurs éphémères, plateformes managées, une seule base à opérer/sauvegarder) | pg |
Driver = dépendance optionnelle
Le kit ne vous impose aucun driver : better-sqlite3 et pg sont des peer dependencies optionnelles. Installez celui de votre backend, car une intégration PostgreSQL ne compile jamais de module natif.
SQLite : le défaut zéro-config
Un fichier local, rien à opérer. C'est le comportement historique du kit, inchangé :
yarn add @shopimind/integration-kit-js better-sqlite3const app = await createIntegrationApp(myIntegration, {
databasePath: env.DATABASE_PATH ?? './data/store.sqlite',
// …
});Montez ./data sur un volume persistant (le fichier SQLite est l'état de votre intégration). Pour un contrôle fin (horloge injectée en test, accès outillé), le même store se construit explicitement :
import { createSqliteStore } from '@shopimind/integration-kit-js/store-sqlite';
const store = await createSqliteStore({ path: './data/store.sqlite' });
const app = await createIntegrationApp(myIntegration, { store, /* … */ });PostgreSQL : votre base, un schéma dédié
Pointez le kit sur la base PostgreSQL que vous opérez déjà : plus aucun fichier local, plus de filesystem persistant à prévoir, un seul backup. Les tables du kit vivent dans un schéma dédié de votre base. Rien d'autre n'y est touché.
yarn add @shopimind/integration-kit-js pgimport { createIntegrationApp } from '@shopimind/integration-kit-js';
import { createPostgresStore } from '@shopimind/integration-kit-js/store-postgres';
const app = await createIntegrationApp(myIntegration, {
store: await createPostgresStore({
connectionString: env.DATABASE_URL!, // postgres://user:pass@host/db
schema: 'shopimind_mon_integration', // un schéma PAR intégration
}),
// …les autres options ne changent pas
});Options de createPostgresStore :
| Option | Défaut | Rôle |
|---|---|---|
connectionString | — | URL de votre base. Le store ouvre (et possède) son propre petit pool. |
pool | — | Alternative : réutiliser un pg.Pool existant de votre application (le kit ne le fermera pas). Exactement l'un des deux est requis. |
schema | shopimind_kit | Schéma PostgreSQL des tables du kit (créé s'il manque). Nommez-le d'après votre intégration, par exemple shopimind_hiboutik. |
maxConnections | 10 | Taille du pool quand le store le possède. |
connectionTimeoutMs | 5000 | Attente maximale d'une connexion libre. Borne les appels au store quand le pool est saturé ou le serveur injoignable. |
statementTimeoutMs | 30000 | statement_timeout côté serveur : une requête bloquée sur un verrou est annulée au lieu de monopoliser le pool. |
pingTimeoutMs | 3000 | Borne du ping() de la sonde /health. Une sonde doit répondre, même si PostgreSQL ne répond plus. |
onPoolError | — | Appelé quand le pool signale une erreur sur une connexion inactive (bascule d'un PostgreSQL managé, timeout PgBouncer). Le store attache toujours un écouteur (sans lui, pg ferait mourir le processus) et vous transmet l'erreur pour que vous puissiez la journaliser. |
Vous n'avez rien à préparer dans la base. Au démarrage, le connecteur crée son schéma et ses tables s'ils n'existent pas, puis applique les migrations qui manquent. L'opération est idempotente : redémarrer ne rejoue rien. Elle se déroule dans une transaction et est protégée par un verrou, donc deux instances qui démarrent en même temps ne peuvent pas entrer en conflit. L'une attend simplement l'autre.
La seule exigence porte sur les droits : le rôle PostgreSQL de connectionString doit pouvoir créer un schéma dans la base (CREATE sur la base) au tout premier démarrage. Si votre politique l'interdit, créez le schéma vide à la main et donnez au rôle les droits dessus ; le kit se chargera des tables.
Ce qui est chiffré, quel que soit le backend
Le chiffrement des secrets est applicatif, au-dessus du store : les valeurs sensibles (setSecret, champs sensitive du config_schema, jeton d'API de la boutique) sont chiffrées AES-256-GCM avant d'atteindre le backend. Un adapter, y compris votre base PostgreSQL partagée, ne voit jamais un secret en clair.
Le reste (domaines de boutique, curseurs, journaux) est stocké en clair : la confidentialité au repos de ces données relève de votre politique de stockage (droits sur le schéma, chiffrement du disque ou du cluster), exactement comme avec le fichier SQLite.
Avant de déployer
N'exécutez qu'une seule instance à la fois
Deux instances du même connecteur lancées en parallèle synchroniseraient les mêmes installations en double. Le kit sait empêcher deux synchros de se chevaucher, mais ce verrou vit en mémoire : il protège un processus contre lui-même, pas deux processus l'un contre l'autre.
En Kubernetes, cela se traduit par replicas: 1 et strategy: Recreate (ou maxSurge: 0). Le réglage par défaut, lui, démarre le nouveau pod avant d'arrêter l'ancien : pendant ces quelques secondes, deux instances tournent.
Avec SQLite, la question ne se posait pas. Un volume ne se monte que sur un pod à la fois. Avec PostgreSQL, plus rien ne l'empêche techniquement : c'est désormais à votre déploiement de le garantir.
Si votre plateforme met le service en veille
Le planificateur de synchro du kit est un minuteur dans le processus : il ne tourne que tant que le processus vit. Sur une plateforme qui descend à zéro instance quand le trafic s'arrête, les synchros automatiques ne partiront pas.
Dans ce cas, désactivez-le et pilotez les synchros de l'extérieur :
const app = await createIntegrationApp(myIntegration, {
store,
autoSync: false, // pas de planificateur interne
// …
});Un cron externe appelle alors POST /admin/sync/{id} (cf. Console d'exploitation), ou, si vous écrivez votre propre déclencheur, app.runSyncOnce(installationId).
Si vous venez du kit v1 (SQLite)
Votre fichier est réutilisé tel quel : mêmes tables, mêmes installations, mêmes secrets chiffrés, mêmes curseurs, même anti-rejeu. Vous passez au kit v2 et vous redémarrez, rien d'autre.
Une seule chose se produit au premier démarrage : une migration réécrit les horodatages enregistrés par la v1 (au format SQL YYYY-MM-DD HH:MM:SS) dans le format ISO-8601 UTC qu'utilise la v2. C'est ce qui garantit que les purges de rétention et les compteurs restent exacts. Comptez quelques secondes de plus sur un gros store, une seule fois.
Sauvegardez le fichier avant de basculer
Cette réécriture ne se défait pas. Revenir au kit v1 reste possible (il relit tout), mais sa sonde /health ne saura plus calculer l'âge des synchros écrites par la v2. En clair : le vrai chemin de retour, c'est la restauration de votre sauvegarde, pas seulement le retour à l'image précédente.
Si vous passez de SQLite à PostgreSQL
Le kit ne transfère pas vos données d'un backend à l'autre : changer de store, c'est repartir d'un stockage vide. Or ce stockage contient les jetons d'API des boutiques installées : sans lui, votre connecteur ne sait plus pour quelle boutique il travaille.
- Avant la mise en service (aucune installation réelle) : rien à faire, changez la configuration et redémarrez.
- En service : soit vous faites réinstaller l'intégration par les marchands concernés (ShopiMind rejoue alors l'installation et l'activation), soit vous transférez vous-même les données. Le transfert est faisable car les deux backends ont le même schéma de tables, et les secrets restent déchiffrables tant que vous conservez la même
credentialsKey.
Écrire son propre store (avancé)
Le port est un point d'extension officiel : n'importe quel backend peut porter le kit, en implémentant l'interface IntegrationStore (exportée par la racine du paquet). Sa surface est volontairement simple :
- du stockage pur, pas de logique métier : le chiffrement, les bornes de pagination, la sérialisation défensive restent dans le kit, au-dessus du port ;
- un appel = une écriture atomique : aucune API de transaction à implémenter ; seules les méthodes
claim(...)ont une exigence de concurrence (« insère si absent », atomique) ; - des timestamps texte ISO-8601 UTC générés par le kit, donc aucune fonction SQL de date à fournir.
Validez votre adapter avec la suite de conformité, le contrat exécutable que les deux adapters officiels passent en CI :
// my-store.conformance.test.ts (vitest ou jest)
import { describe, it, expect } from 'vitest';
import { runStoreConformanceSuite } from '@shopimind/integration-kit-js/store-testing';
import { createMyStore } from './my-store.js';
runStoreConformanceSuite(() => createMyStore(), { describe, it, expect });Elle vérifie notamment l'atomicité des claim sous concurrence, l'invariant « la prévisualisation d'un secret est toujours null », les purges par date, la recherche littérale (% et _ ne sont pas des jokers) et la stabilité de la pagination.
Politique de compatibilité du port
Le port peut gagner des méthodes dans une version mineure du kit (les adapters officiels sont mis à jour en même temps). Re-jouez la suite de conformité sur votre adapter à chaque montée de version. Les retraits ou changements de signature n'arrivent qu'en version majeure.
Pour aller plus loin
- Le kit d'intégration : toutes les options de
createIntegrationApp. - Console d'exploitation : l'API admin (dont
POST /admin/sync/{id}). - Le
CHANGELOGdu paquet : le guide de migration v1 → v2 (l'API du store est devenue asynchrone).