Stockage et adaptateurs
Choisir PostgreSQL ou Convex, brancher votre base ou écrire votre adaptateur.
Choisir son stockage
PONT_STORE | Pour qui | Détail |
|---|---|---|
postgres + DATABASE_URL vide (défaut) | Tout dans Docker, portefeuilles et registre inclus | Conteneur PostgreSQL fourni, volume postgres-data |
convex | Paiements seulement, aucune base à gérer | Convex self-hosted, installé avec Pont. Pas de portefeuille ni de registre |
postgres + DATABASE_URL=… | Brancher votre propre base (Neon, Supabase, RDS, serveur interne) | Aucun conteneur de base n'est lancé |
| adaptateur maison | MySQL, MongoDB, DynamoDB… | Voir adaptateurs de stockage |
memory | Tests locaux uniquement | Les données disparaissent à l'arrêt. Interdit en live. |
Les comptes du dashboard sont à part : un fichier SQLite (volume dashboard-data), géré par Better Auth.
Le serveur Pont ne sait pas où sont stockées les données : il parle à des interfaces. Un adaptateur de stockage fournit l'implémentation. Trois sont livrés ; vous pouvez écrire le vôtre.
| Adaptateur | PONT_STORE | Durable | Où |
|---|---|---|---|
| Mémoire | memory | non | memoryStorage : tests uniquement |
| Convex | convex | oui | @pont/store-convex (Convex self-hosted ou cloud) |
| PostgreSQL | postgres | oui | @pont/store-prisma (via Prisma) |
En mode live, un adaptateur non durable est refusé au démarrage.
Brancher votre PostgreSQL
Aucun code : une variable suffit.
# deploy/self-host/.env
PONT_STORE=postgres
DATABASE_URL=postgresql://utilisateur:motdepasse@hote:5432/base./setup.sh --store postgresSi DATABASE_URL est renseignée, aucun conteneur PostgreSQL n'est lancé : Pont utilise votre base et y crée ses tables (préfixe pont_, voir packages/store-prisma/prisma/schema.prisma) avec prisma db push. Utilisez un compte de base dédié. Les enregistrements sont stockés en JSONB ; des colonnes à côté servent aux index.
Sans Docker :
export DATABASE_URL=postgresql://…
pnpm --filter @pont/store-prisma exec prisma db push
PONT_STORE=postgres pnpm --filter @pont/server startÉcrire votre propre adaptateur
Pour MySQL, MongoDB, DynamoDB ou toute autre base, implémentez les cinq interfaces du moteur (@pont/engine) et retournez-les dans un StorageAdapter. Le serveur s'utilise alors comme une bibliothèque :
import { createRuntime, type StorageFactory } from "@pont/server";
const monStockage: StorageFactory = async (env) => ({
payments: new MonPaymentStore(/* … */),
payouts: new MonPayoutStore(/* … */),
refunds: new MonRefundStore(/* … */),
idempotency: new MonIdempotencyStore(/* … */),
deliveries: new MonDeliveryStore(/* … */),
durable: true,
close: async () => { /* fermer les connexions */ },
});
const runtime = await createRuntime(process.env, { storage: monStockage });
// runtime.handle(request) : (Request) => Promise<Response>, utilisable avec Node, Bun, Deno, Workers…Les interfaces à implémenter
RecordStore<T> : PaymentStore, PayoutStore et RefundStore (qui ajoute listByPayment).
| Méthode | Comportement attendu |
|---|---|
get(id) | L'enregistrement ou null |
findByAttempt(provider, attemptId) | Retrouve par identifiant de tentative |
findByProviderRef(provider, providerRef) | Retrouve par référence de l'agrégateur |
insert(record) | Échoue si l'id existe déjà |
update(record) | Échoue si l'id n'existe pas |
listUnsettled(updatedBefore, limit) | Opérations non réglées (voir isUnsettled), les plus anciennes d'abord : sert à la réconciliation |
listRecent(limit) | Les plus récemment mises à jour d'abord : sert au dashboard |
IdempotencyStore
get(key)renvoie{ fingerprint, recordId }ounull.putIfAbsent(key, record)doit être atomique et renvoyerfalsesi la clé existe. C'est ce qui garantit qu'aucune requête concurrente ne crée deux paiements : utilisez une contrainte d'unicité ou une transaction sérialisable, jamais « lire puis écrire ».
DeliveryStore (livraison des webhooks sortants) : insert, update, get, due(now, limit) (livraisons pending dont nextAttemptAt <= now).
Règles à respecter
- Stockez l'objet complet (JSON) : les
metadatadu marchand sont libres. - Aucune clé d'agrégateur ne passe par le stockage : seulement les opérations (numéro en E.164, montants, statuts).
- Les montants sont des entiers.
- Ne renvoyez jamais d'objets partagés mutables : renvoyez des copies.
- Voyez
MemoryRecordStore(packages/engine/src/store.ts) comme référence de comportement, etpackages/store-prisma/test/store.test.tscomme modèle de tests, y compris le test de concurrence sur l'idempotence.
Il n'y a pas encore de suite de tests de conformité réutilisable : copiez et adaptez ces tests pour valider votre adaptateur.
Champs facultatifs de l'adaptateur
ledger:{ store, profiles }(registre en partie double et profils clients). Absent : le portefeuille n'est pas disponible avec ce stockage (501 ledger_unavailable).secrets: coffre de secrets (get,put,delete,list). Il ne reçoit que des valeurs déjà chiffrées. Absent : les clés d'agrégateurs restent en variables d'environnement.workflowRuns: reprise des retraits interrompus. En mémoire si absent.
Les comptes du dashboard (Better Auth) ont leur propre base SQLite et ne passent pas par l'adaptateur.