10 décembre 2025. Abidjan, Côte d'Ivoire. 0fee.dev n'existait pas encore -- pas une seule ligne de code, pas un seul fichier. Quarante-cinq minutes plus tard, un backend complet d'orchestration de paiement fonctionnait : 42 fichiers, environ 7 900 lignes de Python, 5 fournisseurs de paiement, 30+ endpoints d'API REST, et un schéma de base de données avec 15+ tables. Voici l'histoire de la Session 001.
Le point de départ : une spécification et rien d'autre
Avant le début de la session, ce qui existait était un plan d'implémentation en 14 sections -- une spécification technique détaillée couvrant l'architecture, les intégrations de fournisseurs, la logique de routage, le schéma de base de données et les endpoints d'API. Le plan était complet. La base de code était vide.
La directive était simple : construire l'intégralité du backend FastAPI pour une plateforme unifiée d'orchestration de paiement. Pas un prototype. Pas un squelette. Un système fonctionnel avec de vraies intégrations de fournisseurs, une vraie authentification, de vrais middlewares et de vraies routes API.
Phase par phase : comment le backend s'est matérialisé
La construction a suivi une séquence stricte en sept phases. Chaque phase dépendait de la précédente, et chacune était terminée avant de passer à la suivante.
Phase 1 : Infrastructure de base (config, base de données, cache)
Les fondations d'abord. Trois fichiers dont tout le reste dépendrait.
backend/config.py -- Gestion des paramètres avec Pydantic, chargeant chaque variable d'environnement dont la plateforme aurait besoin : chemins de base de données, URL de cache, secrets JWT, clés API des fournisseurs, clés de chiffrement.
pythonfrom pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Base de données
DATABASE_PATH: str = "0fee.db"
# Cache (DragonflyDB / Redis)
CACHE_URL: str = "redis://localhost:6379"
# JWT
JWT_SECRET: str = "change-me-in-production"
JWT_EXPIRY_HOURS: int = 24
# Chiffrement
ENCRYPTION_KEY: str = "change-me-in-production"
# Clés des fournisseurs
STRIPE_SECRET_KEY: str = ""
PAYPAL_CLIENT_ID: str = ""
PAYPAL_CLIENT_SECRET: str = ""
HUB2_API_KEY: str = ""
PAWAPAY_API_KEY: str = ""
class Config:
env_file = ".env"
settings = Settings()backend/database.py -- SQLite avec le mode WAL (Write-Ahead Logging) et un schéma complet. Le mode WAL était le choix critique : il permet des lectures concurrentes pendant qu'un seul écrivain opère, ce qui est suffisant pour une plateforme de paiement traitant des milliers de transactions par jour. Le schéma définissait 15+ tables couvrant les utilisateurs, les applications, les clés API, les identifiants des fournisseurs, les transactions, les portefeuilles, les webhooks et la configuration du routage.
pythonimport sqlite3
from contextlib import contextmanager
DB_PATH = "0fee.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
full_name TEXT,
phone TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS apps (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
environment TEXT DEFAULT 'sandbox',
webhook_url TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
app_id TEXT NOT NULL REFERENCES apps(id),
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
scopes TEXT DEFAULT '["payments"]',
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS transactions (
id TEXT PRIMARY KEY,
app_id TEXT NOT NULL REFERENCES apps(id),
provider TEXT NOT NULL,
amount INTEGER NOT NULL,
currency TEXT NOT NULL,
status TEXT DEFAULT 'pending',
payment_method TEXT,
customer_phone TEXT,
customer_email TEXT,
provider_ref TEXT,
metadata TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- ... 11 autres tables pour les portefeuilles, webhooks,
-- provider_credentials, routage, etc.
""")
conn.close()
@contextmanager
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
finally:
conn.close()backend/cache.py -- Un client Redis/DragonflyDB asynchrone pour les sessions, la limitation de débit, les codes OTP et les clés d'idempotence. DragonflyDB a été choisi plutôt que Redis standard pour son efficacité mémoire supérieure et son architecture multi-thread.
Phase 2 : Modèles Pydantic
Avec l'infrastructure en place, la couche de données est venue ensuite. Sept fichiers de modèles définissaient chaque structure de données que l'API utiliserait.
| Fichier | Objectif | Modèles clés |
|---|---|---|
models/base.py | Énumérations et réponses de base | TransactionStatus, PaymentMethod, Currency, ApiResponse |
models/user.py | Authentification | UserRegister, UserLogin, UserResponse |
models/app.py | Applications multi-tenant | AppCreate, AppResponse, ApiKeyCreate |
models/transaction.py | Paiements | PaymentInitiate, PaymentResponse, PaymentStatus |
models/provider.py | Capacités des fournisseurs | ProviderCapability, CorrespondentCode |
models/webhook.py | Livraison d'événements | WebhookEvent, WebhookDelivery |
models/wallet.py | Gestion financière | WalletBalance, WithdrawalRequest |
Les modèles Pydantic servaient double emploi : validation des entrées pour les requêtes API et sérialisation pour les réponses API. Chaque champ avait des annotations de type, des validateurs et des valeurs par défaut. Cela signifiait qu'au moment où une requête atteignait un gestionnaire de route, les données étaient déjà validées et typées.
Phase 3 : Fournisseurs de paiement
C'était la phase la plus complexe -- implémenter cinq adaptateurs de fournisseurs de paiement distincts, chacun encapsulant une API externe différente avec des schémas d'authentification différents, des flux de paiement différents et des formats de webhook différents.
La classe de base (providers/base.py) définissait le contrat que chaque adaptateur de fournisseur devait respecter :
pythonfrom abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class InitPaymentResult:
provider_ref: str
status: str # "pending", "redirect", "ussd_push", "failed"
redirect_url: Optional[str] = None
ussd_code: Optional[str] = None
instructions: Optional[str] = None
class BasePayinProvider(ABC):
provider_id: str
provider_name: str
supported_countries: list[str]
supported_methods: list[str]
@abstractmethod
async def initiate_payment(self, data: dict) -> InitPaymentResult:
"""Démarrer un paiement. Renvoie le résultat avec le statut et les informations du flux."""
pass
@abstractmethod
async def get_status(self, provider_ref: str) -> dict:
"""Vérifier le statut du paiement auprès du fournisseur."""
pass
@abstractmethod
async def handle_webhook(self, payload: dict, headers: dict) -> dict:
"""Traiter un webhook entrant du fournisseur."""
pass
async def refund(self, provider_ref: str, amount: Optional[int] = None) -> dict:
raise NotImplementedError("Refund not supported")
async def cancel_payment(self, provider_ref: str) -> dict:
raise NotImplementedError("Cancel not supported")
async def validate_credentials(self) -> bool:
raise NotImplementedError("Credential validation not supported")Puis cinq fournisseurs, chacun implémentant cette interface :
| Fournisseur | Type de flux | Pays | Complexité clé |
|---|---|---|---|
| Test | Instantané | Tous | Montants magiques : 10000 = succès, 99999 = échec |
| Stripe | Redirection | Mondial | API Checkout Sessions, vérification de signature webhook |
| PayPal | Redirection | Mondial | API Orders, gestion de tokens OAuth2 |
| Hub2 | Push USSD | CI, SN, BJ, BF, CM, ML, TG, GN | Push USSD vers le téléphone du client, polling de statut |
| PawaPay | Page hébergée | 21+ pays africains | Page de paiement hébergée, mapping des codes correspondants |
Chaque répertoire de fournisseur suivait la même structure : un __init__.py et un provider.py implémentant la classe de base. Le fournisseur Test était particulièrement utile -- il utilisait des « montants magiques » pour simuler différents résultats de paiement de manière déterministe, permettant de tester les flux de succès, d'échec et de timeout sans toucher aux vraies API de paiement.
Phase 4 : Middlewares
Trois composants middleware que chaque requête API traverserait :
Authentification (middleware/auth.py) -- Extraction et validation des clés API. Les clés suivent une convention de préfixe : sk_live_ pour les clés secrètes de production, sk_sand_ pour le sandbox, pk_live_ pour les clés publiques de production, pk_sand_ pour le sandbox. Le préfixe détermine l'environnement et la portée de la requête sans consultation de la base de données.
Limitation de débit (middleware/rate_limit.py) -- Limitation de débit par fenêtre glissante basée sur Redis. Chaque clé API obtient un budget de requêtes configurable par fenêtre temporelle. L'implémentation incluait une dégradation gracieuse : si Redis est indisponible, les requêtes passent plutôt que d'échouer. Une plateforme de paiement ne doit jamais bloquer le trafic légitime parce que le limiteur de débit est en panne.
Idempotence (middleware/idempotency.py) -- Gestion des clés d'idempotence pour prévenir les paiements en double. Si un client envoie le même en-tête Idempotency-Key deux fois, la seconde requête renvoie la réponse mise en cache de la première. C'est critique pour les systèmes de paiement où les timeouts réseau peuvent amener les clients à relancer des requêtes qui ont déjà réussi.
Phase 5 : Services
Deux modules de services contenant la logique métier qui traverse plusieurs routes :
Routage (services/routing.py) -- Le moteur de routage intelligent des paiements qui détermine quel fournisseur utiliser pour un paiement donné. Étant donné un pays et une méthode de paiement, il interroge la table de routage pour les fournisseurs disponibles, les ordonne par priorité et renvoie la meilleure correspondance. Si le fournisseur principal échoue, l'appelant peut demander le suivant dans la chaîne de repli.
Chiffrement (services/encryption.py) -- Chiffrement Fernet/AES pour les identifiants des fournisseurs. Lorsqu'un marchand stocke sa clé API Stripe ou sa clé d'abonnement Hub2 dans 0fee.dev, cet identifiant est chiffré au repos. Le service de chiffrement gère la dérivation des clés, le chiffrement et le déchiffrement de manière transparente.
Phase 6 : Routes API
Six modules de routes exposant 30+ endpoints :
GET / # Infos API
GET /health # Vérification de santé basique
GET /health/ready # Disponibilité (BD + Cache)
GET /health/live # Vivacité
POST /v1/auth/register # Inscription utilisateur
POST /v1/auth/login # Connexion utilisateur
POST /v1/auth/otp/request # Demander un OTP
POST /v1/auth/otp/verify # Vérifier un OTP
POST /v1/auth/refresh # Rafraîchir le token
POST /v1/auth/logout # Déconnexion
GET /v1/apps # Lister les applications
POST /v1/apps # Créer une application
GET /v1/apps/{app_id} # Obtenir une application
PATCH /v1/apps/{app_id} # Mettre à jour une application
GET /v1/apps/{app_id}/keys # Lister les clés API
POST /v1/apps/{app_id}/keys # Créer une clé API
DELETE /v1/apps/{app_id}/keys/{key_id} # Révoquer une clé
GET /v1/apps/{app_id}/providers # Lister les fournisseurs
POST /v1/apps/{app_id}/providers # Ajouter un fournisseur
DELETE /v1/apps/{app_id}/providers/{id} # Retirer un fournisseur
GET /v1/apps/{app_id}/routes # Lister les routes
POST /v1/apps/{app_id}/routes # Créer une route
DELETE /v1/apps/{app_id}/routes/{id} # Supprimer une route
POST /v1/payments # Initier un paiement
GET /v1/payments # Lister les paiements
GET /v1/payments/{payment_id} # Obtenir un paiement
POST /v1/payments/{payment_id}/authenticate # Soumettre un OTP
POST /v1/payments/{payment_id}/cancel # Annuler un paiement
GET /v1/checkout/payment-methods # Méthodes pour un pays
POST /v1/checkout/sessions # Créer une session
GET /v1/checkout/sessions/{session_id} # Obtenir une session
GET /v1/checkout/countries # Pays supportés
POST /v1/webhooks/{provider_id} # Webhook fournisseurPhase 7 : Application principale et configuration
La phase finale a tout relié. backend/main.py définissait l'application FastAPI avec des événements de cycle de vie (initialisation de la base de données au démarrage, nettoyage du cache à l'arrêt), montait tous les routeurs et configurait le CORS. Un requirements.txt listait toutes les dépendances Python. Un .env.example documentait chaque variable d'environnement.
L'arborescence complète des fichiers
Voici chaque fichier créé lors de la Session 001 :
backend/
├── __init__.py
├── main.py # Point d'entrée FastAPI
├── config.py # Paramètres Pydantic
├── database.py # SQLite + mode WAL
├── cache.py # Client DragonflyDB
├── requirements.txt # Dépendances Python
├── .env.example # Modèle d'environnement
├── models/
│ ├── __init__.py
│ ├── base.py # Énumérations, réponses de base
│ ├── user.py # Modèles d'authentification
│ ├── app.py # Modèles application/tenant
│ ├── transaction.py # Modèles de paiement
│ ├── provider.py # Modèles de capacités fournisseur
│ ├── webhook.py # Modèles d'événements webhook
│ └── wallet.py # Modèles de portefeuille
├── providers/
│ ├── __init__.py
│ ├── base.py # Classe abstraite de base
│ ├── registry.py # Registre des fournisseurs
│ ├── test/
│ │ ├── __init__.py
│ │ └── provider.py # Fournisseur de test (montants magiques)
│ ├── stripe/
│ │ ├── __init__.py
│ │ └── provider.py # Stripe Checkout Sessions
│ ├── paypal/
│ │ ├── __init__.py
│ │ └── provider.py # PayPal Orders API
│ ├── hub2/
│ │ ├── __init__.py
│ │ └── provider.py # Hub2 (Afrique francophone)
│ └── pawapay/
│ ├── __init__.py
│ └── provider.py # PawaPay (21+ pays)
├── middleware/
│ ├── __init__.py
│ ├── auth.py # Authentification par clé API
│ ├── rate_limit.py # Limitation de débit basée sur Redis
│ └── idempotency.py # Prévention des paiements en double
├── services/
│ ├── __init__.py
│ ├── routing.py # Moteur de routage des paiements
│ └── encryption.py # Chiffrement des identifiants
└── routes/
├── __init__.py
├── health.py # Vérifications de santé
├── auth.py # Endpoints d'authentification
├── apps.py # Gestion des applications
├── payments.py # Opérations de paiement
├── checkout.py # Sessions de checkout
└── webhooks.py # Webhooks des fournisseurs42 fichiers. Zéro générateur de boilerplate. Zéro copier-coller depuis des templates. Chaque fichier a été écrit from scratch à partir de la spécification d'implémentation.
Les chiffres
| Métrique | Valeur |
|---|---|
| Total des fichiers | 42 |
| Total des lignes de code | ~7 900 |
| Endpoints API | 30+ |
| Fournisseurs de paiement | 5 (Test, Stripe, PayPal, Hub2, PawaPay) |
| Tables de base de données | 15+ |
| Modèles Pydantic | 40+ |
| Composants middleware | 3 |
| Modules de services | 2 |
| Temps écoulé | ~45 minutes |
Pourquoi SQLite avec le mode WAL ?
Une plateforme d'orchestration de paiement tournant sur SQLite pourrait surprendre. Le raisonnement était pragmatique :
- Zéro configuration. Pas de serveur PostgreSQL à provisionner, pas de pool de connexions à configurer, pas d'identifiants à gérer. La base de données est un seul fichier.
- Le mode WAL gère la concurrence. Plusieurs lecteurs peuvent opérer simultanément pendant qu'un écrivain détient le verrou. Pour une plateforme à ses débuts, c'est plus que suffisant.
- Portabilité. Toute la base de données peut être sauvegardée en copiant un seul fichier. Changer d'environnement signifie copier un fichier
.db. - La performance est adéquate. SQLite avec WAL peut gérer des centaines d'écritures par seconde. Une nouvelle plateforme de paiement n'a pas besoin de gérer des milliers d'écritures concurrentes dès le premier jour.
Le plan incluait toujours un chemin de migration vers PostgreSQL -- et cette migration a finalement eu lieu lors de la Session 081. Mais commencer avec SQLite signifiait que le backend pouvait être construit, testé et déployé sans aucune dépendance externe de base de données.
Pourquoi DragonflyDB plutôt que Redis ?
DragonflyDB est un remplacement drop-in de Redis qui utilise une architecture multi-thread au lieu de la boucle événementielle mono-thread de Redis. Pour 0fee.dev, les avantages pratiques étaient :
- Utilisation mémoire réduite -- DragonflyDB utilise jusqu'à 80 % de mémoire en moins pour le même jeu de données.
- Même protocole -- Chaque bibliothèque cliente Redis fonctionne sans modification.
- Meilleures performances sous charge -- Le modèle multi-thread s'adapte avec les cœurs du processeur.
Puisque le cache était utilisé pour les sessions, la limitation de débit, les codes OTP et les clés d'idempotence -- toutes des opérations à haute fréquence et faible latence -- le choix du moteur de cache comptait. DragonflyDB offrait de meilleures performances avec moins de mémoire, à un coût de code nul.
Ce qui a rendu cela possible
Construire un backend de paiement complet en 45 minutes n'est pas normal. Plusieurs facteurs l'ont rendu faisable :
Une spécification approfondie. Le plan d'implémentation en 14 sections répondait à chaque question architecturale avant l'écriture de la première ligne de code. Il n'y avait aucune ambiguïté sur ce qu'il fallait construire.
Une séquence de phases claire. Chaque phase se construisait sur la précédente. Le graphe de dépendances était linéaire, ce qui signifiait pas de retour en arrière, pas de dépendances circulaires, pas de redesign en cours de session.
Python et FastAPI. L'expressivité de Python et le routage basé sur les décorateurs de FastAPI minimisent le boilerplate. Un gestionnaire de route qui prendrait 30 lignes en Java en prend 10 en Python.
Le pattern adaptateur. Les cinq fournisseurs de paiement implémentaient la même interface. Une fois la classe de base définie, chaque fournisseur était un module autonome qui pouvait être écrit sans toucher à aucun autre code.
C'était la Session 001. Le backend existait. La session suivante ajouterait deux fournisseurs supplémentaires, un tableau de bord SolidJS complet, un widget de checkout, des tâches Celery en arrière-plan et deux SDK -- en 60 minutes.
Cet article fait partie de la série « Comment nous avons construit 0fee.dev ». 0fee.dev est un orchestrateur de paiement couvrant 53+ fournisseurs dans 200+ pays, construit par Juste A. GNIMAVO et Claude depuis Abidjan sans aucun ingénieur humain. Suivez la série pour l'histoire complète de la construction.