10 de diciembre de 2025. Abiyán, Costa de Marfil. 0fee.dev aún no existía -- ni una sola línea de código, ni un solo archivo. Cuarenta y cinco minutos después, un backend completo de orquestación de pagos estaba funcionando: 42 archivos, aproximadamente 7.900 líneas de Python, 5 proveedores de pago, 30+ endpoints de API REST y un esquema de base de datos con 15+ tablas. Esta es la historia de la Sesión 001.
El punto de partida: una especificación y nada más
Antes de que comenzara la sesión, lo que existía era un plan de implementación de 14 secciones -- una especificación técnica detallada que cubría arquitectura, integraciones de proveedores, lógica de enrutamiento, esquema de base de datos y endpoints de API. El plan era exhaustivo. La base de código estaba vacía.
La directiva fue simple: construir todo el backend en FastAPI para una plataforma unificada de orquestación de pagos. No un prototipo. No un esqueleto. Un sistema funcional con integraciones reales de proveedores, autenticación real, middleware real y rutas de API reales.
Fase por fase: cómo se materializó el backend
La construcción siguió una secuencia estricta de siete fases. Cada fase dependía de la anterior, y cada una se completó antes de pasar a la siguiente.
Fase 1: Infraestructura central (configuración, base de datos, caché)
La base vino primero. Tres archivos de los que todo lo demás dependería.
backend/config.py -- Gestión de configuración con Pydantic cargando cada variable de entorno que la plataforma necesitaría: rutas de base de datos, URLs de caché, secretos JWT, claves de API de proveedores, claves de cifrado.
pythonfrom pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Base de datos
DATABASE_PATH: str = "0fee.db"
# Caché (DragonflyDB / Redis)
CACHE_URL: str = "redis://localhost:6379"
# JWT
JWT_SECRET: str = "change-me-in-production"
JWT_EXPIRY_HOURS: int = 24
# Cifrado
ENCRYPTION_KEY: str = "change-me-in-production"
# Claves de proveedores
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 con modo WAL (Write-Ahead Logging) y un esquema completo. El modo WAL fue la elección crítica aquí: permite lectores concurrentes mientras un solo escritor opera, lo cual es suficiente para una plataforma de pagos que maneja miles de transacciones por día. El esquema definió 15+ tablas cubriendo usuarios, apps, claves de API, credenciales de proveedores, transacciones, billeteras, webhooks y configuración de enrutamiento.
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 tablas más para billeteras, webhooks,
-- provider_credentials, enrutamiento, 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 cliente async de Redis/DragonflyDB para sesiones, límites de tasa, códigos OTP y claves de idempotencia. DragonflyDB fue elegido sobre Redis estándar por su eficiencia de memoria superior y arquitectura multi-hilo.
Fase 2: Modelos Pydantic
Con la infraestructura en su lugar, la capa de datos vino después. Siete archivos de modelos definieron cada estructura de datos que usaría la API.
| Archivo | Propósito | Modelos clave |
|---|---|---|
models/base.py | Enums y respuestas base | TransactionStatus, PaymentMethod, Currency, ApiResponse |
models/user.py | Autenticación | UserRegister, UserLogin, UserResponse |
models/app.py | Apps multi-inquilino | AppCreate, AppResponse, ApiKeyCreate |
models/transaction.py | Pagos | PaymentInitiate, PaymentResponse, PaymentStatus |
models/provider.py | Capacidades del proveedor | ProviderCapability, CorrespondentCode |
models/webhook.py | Entrega de eventos | WebhookEvent, WebhookDelivery |
models/wallet.py | Gestión financiera | WalletBalance, WithdrawalRequest |
Los modelos Pydantic sirvieron doble propósito: validación de entrada para solicitudes de API y serialización para respuestas de API. Cada campo tenía anotaciones de tipo, validadores y valores predeterminados. Esto significaba que para cuando una solicitud llegaba a un manejador de ruta, los datos ya estaban validados y tipados.
Fase 3: Proveedores de pago
Esta fue la fase más compleja -- implementar cinco adaptadores distintos de proveedores de pago, cada uno envolviendo una API externa diferente con diferentes esquemas de autenticación, diferentes flujos de pago y diferentes formatos de webhook.
La clase base (providers/base.py) definió el contrato que cada proveedor debía cumplir:
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:
"""Iniciar un pago. Retorna resultado con estado e información del flujo."""
pass
@abstractmethod
async def get_status(self, provider_ref: str) -> dict:
"""Verificar estado del pago con el proveedor."""
pass
@abstractmethod
async def handle_webhook(self, payload: dict, headers: dict) -> dict:
"""Procesar webhook entrante del proveedor."""
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")Luego cinco proveedores, cada uno implementando esta interfaz:
| Proveedor | Tipo de flujo | Países | Complejidad clave |
|---|---|---|---|
| Test | Instantáneo | Todos | Montos mágicos: 10000 = éxito, 99999 = fallo |
| Stripe | Redirección | Global | API de Checkout Sessions, verificación de firma de webhook |
| PayPal | Redirección | Global | API de Orders, gestión de tokens OAuth2 |
| Hub2 | USSD Push | CI, SN, BJ, BF, CM, ML, TG, GN | USSD push al teléfono del cliente, polling de estado |
| PawaPay | Página alojada | 21+ africanos | Página de pago alojada, mapeo de códigos de corresponsal |
Cada directorio de proveedor siguió la misma estructura: un __init__.py y un provider.py implementando la clase base. El proveedor Test fue particularmente útil -- usaba "montos mágicos" para simular diferentes resultados de pago de manera determinística, haciendo posible probar flujos de éxito, fallo y timeout sin tocar APIs de pago reales.
Fase 4: Middleware
Tres componentes de middleware por los que pasaría cada solicitud de API:
Autenticación (middleware/auth.py) -- Extracción y validación de clave API. Las claves siguen una convención de prefijo: sk_live_ para claves secretas de producción, sk_sand_ para sandbox, pk_live_ para claves publicables de producción, pk_sand_ para sandbox publicable. El prefijo determina el entorno y alcance de la solicitud sin una consulta a la base de datos.
Límite de tasa (middleware/rate_limit.py) -- Límite de tasa basado en Redis con ventana deslizante. Cada clave API obtiene un presupuesto configurable de solicitudes por ventana de tiempo. La implementación incluía degradación elegante: si Redis no está disponible, las solicitudes pasan en lugar de fallar. Una plataforma de pagos nunca debe bloquear tráfico legítimo porque el limitador de tasa está caído.
Idempotencia (middleware/idempotency.py) -- Manejo de clave de idempotencia para prevenir pagos duplicados. Si un cliente envía el mismo encabezado Idempotency-Key dos veces, la segunda solicitud devuelve la respuesta en caché de la primera. Esto es crítico para sistemas de pago donde los timeouts de red pueden causar que los clientes reintenten solicitudes que ya tuvieron éxito.
Fase 5: Servicios
Dos módulos de servicio conteniendo lógica de negocio que abarca múltiples rutas:
Enrutamiento (services/routing.py) -- El motor de enrutamiento inteligente de pagos que determina qué proveedor usar para un pago dado. Dado un país y método de pago, consulta la tabla de enrutamiento para proveedores disponibles, los ordena por prioridad y devuelve la mejor coincidencia. Si el proveedor principal falla, el llamador puede solicitar el siguiente en la cadena de fallback.
Cifrado (services/encryption.py) -- Cifrado Fernet/AES para credenciales de proveedores. Cuando un comerciante almacena su clave de API de Stripe o su clave de suscripción de Hub2 en 0fee.dev, esa credencial se cifra en reposo. El servicio de cifrado maneja la derivación de claves, cifrado y descifrado de manera transparente.
Fase 6: Rutas de API
Seis módulos de rutas exponiendo 30+ endpoints:
GET / # Información de la API
GET /health # Verificación de salud básica
GET /health/ready # Disponibilidad (BD + Caché)
GET /health/live # Vivacidad
POST /v1/auth/register # Registro de usuario
POST /v1/auth/login # Login de usuario
POST /v1/auth/otp/request # Solicitar OTP
POST /v1/auth/otp/verify # Verificar OTP
POST /v1/auth/refresh # Refrescar token
POST /v1/auth/logout # Cerrar sesión
GET /v1/apps # Listar apps
POST /v1/apps # Crear app
GET /v1/apps/{app_id} # Obtener app
PATCH /v1/apps/{app_id} # Actualizar app
GET /v1/apps/{app_id}/keys # Listar claves API
POST /v1/apps/{app_id}/keys # Crear clave API
DELETE /v1/apps/{app_id}/keys/{key_id} # Revocar clave
GET /v1/apps/{app_id}/providers # Listar proveedores
POST /v1/apps/{app_id}/providers # Agregar proveedor
DELETE /v1/apps/{app_id}/providers/{id} # Eliminar proveedor
GET /v1/apps/{app_id}/routes # Listar rutas
POST /v1/apps/{app_id}/routes # Crear ruta
DELETE /v1/apps/{app_id}/routes/{id} # Eliminar ruta
POST /v1/payments # Iniciar pago
GET /v1/payments # Listar pagos
GET /v1/payments/{payment_id} # Obtener pago
POST /v1/payments/{payment_id}/authenticate # Enviar OTP
POST /v1/payments/{payment_id}/cancel # Cancelar pago
GET /v1/checkout/payment-methods # Métodos por país
POST /v1/checkout/sessions # Crear sesión
GET /v1/checkout/sessions/{session_id} # Obtener sesión
GET /v1/checkout/countries # Países soportados
POST /v1/webhooks/{provider_id} # Webhook del proveedorFase 7: App principal y configuración
La fase final unió todo. backend/main.py definió la aplicación FastAPI con eventos de ciclo de vida (inicialización de base de datos al arrancar, limpieza de caché al cerrar), montó todos los routers y configuró CORS. Un requirements.txt listó todas las dependencias de Python. Un .env.example documentó cada variable de entorno.
El árbol de archivos completo
Aquí está cada archivo creado en la Sesión 001:
backend/
├── __init__.py
├── main.py # Punto de entrada FastAPI
├── config.py # Configuración Pydantic
├── database.py # SQLite + modo WAL
├── cache.py # Cliente DragonflyDB
├── requirements.txt # Dependencias Python
├── .env.example # Plantilla de entorno
├── models/
│ ├── __init__.py
│ ├── base.py # Enums, respuestas base
│ ├── user.py # Modelos de auth
│ ├── app.py # Modelos de app/inquilino
│ ├── transaction.py # Modelos de pago
│ ├── provider.py # Modelos de capacidad del proveedor
│ ├── webhook.py # Modelos de eventos webhook
│ └── wallet.py # Modelos de billetera
├── providers/
│ ├── __init__.py
│ ├── base.py # Clase base abstracta
│ ├── registry.py # Registro de proveedores
│ ├── test/
│ │ ├── __init__.py
│ │ └── provider.py # Proveedor test (montos mágicos)
│ ├── stripe/
│ │ ├── __init__.py
│ │ └── provider.py # Stripe Checkout Sessions
│ ├── paypal/
│ │ ├── __init__.py
│ │ └── provider.py # PayPal Orders API
│ ├── hub2/
│ │ ├── __init__.py
│ │ └── provider.py # Hub2 (África francófona)
│ └── pawapay/
│ ├── __init__.py
│ └── provider.py # PawaPay (21+ países)
├── middleware/
│ ├── __init__.py
│ ├── auth.py # Autenticación por clave API
│ ├── rate_limit.py # Límite de tasa basado en Redis
│ └── idempotency.py # Prevención de pagos duplicados
├── services/
│ ├── __init__.py
│ ├── routing.py # Motor de enrutamiento de pagos
│ └── encryption.py # Cifrado de credenciales
└── routes/
├── __init__.py
├── health.py # Verificaciones de salud
├── auth.py # Endpoints de auth de usuario
├── apps.py # Gestión de apps
├── payments.py # Operaciones de pago
├── checkout.py # Sesiones de checkout
└── webhooks.py # Webhooks de proveedores42 archivos. Cero generadores de código repetitivo. Cero copy-paste de plantillas. Cada archivo fue escrito desde cero basado en la especificación de implementación.
Las cifras
| Métrica | Valor |
|---|---|
| Total de archivos | 42 |
| Total de líneas de código | ~7.900 |
| Endpoints de API | 30+ |
| Proveedores de pago | 5 (Test, Stripe, PayPal, Hub2, PawaPay) |
| Tablas de base de datos | 15+ |
| Modelos Pydantic | 40+ |
| Componentes de middleware | 3 |
| Módulos de servicio | 2 |
| Tiempo transcurrido | ~45 minutos |
Lo que hizo esto posible
Construir un backend de pagos completo en 45 minutos no es normal. Varios factores lo hicieron factible:
Una especificación exhaustiva. El plan de implementación de 14 secciones respondió cada pregunta arquitectónica antes de que se escribiera la primera línea de código. No hubo ambigüedad sobre qué construir.
Una secuencia de fases clara. Cada fase se construyó sobre la anterior. El grafo de dependencias era lineal, lo que significó que no hubo retroceso, sin dependencias circulares, sin rediseños a mitad de sesión.
Python y FastAPI. La expresividad de Python y el enrutamiento basado en decoradores de FastAPI minimizan el código repetitivo. Un manejador de ruta que tomaría 30 líneas en Java toma 10 en Python.
El patrón adaptador. Los cinco proveedores de pago implementaron la misma interfaz. Una vez definida la clase base, cada proveedor fue un módulo autónomo que podía escribirse sin tocar ningún otro código.
Esta fue la Sesión 001. El backend existía. La siguiente sesión agregaría dos proveedores más, un panel completo en SolidJS, un widget de checkout, tareas de fondo Celery y dos SDKs -- en 60 minutos.
Este artículo es parte de la serie "Cómo construimos 0fee.dev". 0fee.dev es un orquestador de pagos que cubre más de 53 proveedores en más de 200 países, construido por Juste A. GNIMAVO y Claude desde Abiyán sin ningún ingeniero humano. Sigue la serie para conocer la historia completa de construcción.