Back to 0fee
0fee

42 Files, 7,900 Lines, 45 Minutes: The First Session

How we built the entire 0fee.dev backend in 45 minutes: 42 files, 7,900 lines, 5 payment providers, 30+ API endpoints. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 12 min 0fee
session-001backendfastapipythonarchitecture

December 10, 2025. Abidjan, Ivory Coast. 0fee.dev did not exist yet -- not a single line of code, not a single file. Forty-five minutes later, a complete payment orchestration backend was running: 42 files, approximately 7,900 lines of Python, 5 payment providers, 30+ REST API endpoints, and a database schema with 15+ tables. This is the story of Session 001.

The Starting Point: A Specification and Nothing Else

Before the session began, what existed was a 14-section implementation plan -- a detailed technical specification covering architecture, provider integrations, routing logic, database schema, and API endpoints. The plan was thorough. The codebase was empty.

The directive was simple: build the entire FastAPI backend for a unified payment orchestration platform. Not a prototype. Not a skeleton. A working system with real provider integrations, real authentication, real middleware, and real API routes.

Phase by Phase: How the Backend Materialized

The build followed a strict seven-phase sequence. Each phase depended on the previous one, and each was completed before moving to the next.

Phase 1: Core Infrastructure (config, database, cache)

The foundation came first. Three files that everything else would depend on.

backend/config.py -- Pydantic settings management loading every environment variable the platform would need: database paths, cache URLs, JWT secrets, provider API keys, encryption keys.

pythonfrom pydantic_settings import BaseSettings

class Settings(BaseSettings):
    # Database
    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

    # Encryption
    ENCRYPTION_KEY: str = "change-me-in-production"

    # Provider keys
    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 with WAL (Write-Ahead Logging) mode and a complete schema. WAL mode was the critical choice here: it allows concurrent readers while a single writer operates, which is sufficient for a payment platform handling thousands of transactions per day. The schema defined 15+ tables covering users, apps, API keys, provider credentials, transactions, wallets, webhooks, and routing configuration.

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 more tables for wallets, webhooks,
        -- provider_credentials, routing, 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 -- An async Redis/DragonflyDB client for sessions, rate limiting, OTP codes, and idempotency keys. DragonflyDB was chosen over standard Redis for its superior memory efficiency and multi-threaded architecture.

Phase 2: Pydantic Models

With infrastructure in place, the data layer came next. Seven model files defined every data structure the API would use.

FilePurposeKey Models
models/base.pyEnums and base responsesTransactionStatus, PaymentMethod, Currency, ApiResponse
models/user.pyAuthenticationUserRegister, UserLogin, UserResponse
models/app.pyMulti-tenant appsAppCreate, AppResponse, ApiKeyCreate
models/transaction.pyPaymentsPaymentInitiate, PaymentResponse, PaymentStatus
models/provider.pyProvider capabilitiesProviderCapability, CorrespondentCode
models/webhook.pyEvent deliveryWebhookEvent, WebhookDelivery
models/wallet.pyFinancial managementWalletBalance, WithdrawalRequest

The Pydantic models served double duty: input validation for API requests and serialization for API responses. Every field had type annotations, validators, and default values. This meant that by the time a request reached a route handler, the data was already validated and typed.

Phase 3: Payment Providers

This was the most complex phase -- implementing five distinct payment provider adapters, each wrapping a different external API with different authentication schemes, different payment flows, and different webhook formats.

The base class (providers/base.py) defined the contract every provider had to fulfill:

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:
        """Start a payment. Returns result with status and flow info."""
        pass

    @abstractmethod
    async def get_status(self, provider_ref: str) -> dict:
        """Check payment status with the provider."""
        pass

    @abstractmethod
    async def handle_webhook(self, payload: dict, headers: dict) -> dict:
        """Process incoming webhook from provider."""
        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")

Then five providers, each implementing this interface:

ProviderFlow TypeCountriesKey Complexity
TestInstantAllMagic amounts: 10000 = success, 99999 = fail
StripeRedirectGlobalCheckout Sessions API, webhook signature verification
PayPalRedirectGlobalOrders API, OAuth2 token management
Hub2USSD PushCI, SN, BJ, BF, CM, ML, TG, GNUSSD push to customer phone, status polling
PawaPayHosted Page21+ AfricanHosted payment page, correspondent code mapping

Each provider directory followed the same structure: an __init__.py and a provider.py implementing the base class. The Test provider was particularly useful -- it used "magic amounts" to simulate different payment outcomes deterministically, making it possible to test success flows, failure flows, and timeout flows without touching real payment APIs.

Phase 4: Middleware

Three middleware components that every API request would pass through:

Authentication (middleware/auth.py) -- API key extraction and validation. Keys follow a prefix convention: sk_live_ for production secret keys, sk_sand_ for sandbox, pk_live_ for production publishable keys, pk_sand_ for sandbox publishable. The prefix determines the environment and scope of the request without a database lookup.

Rate Limiting (middleware/rate_limit.py) -- Redis-based sliding window rate limiting. Each API key gets a configurable request budget per time window. The implementation included graceful degradation: if Redis is unavailable, requests pass through rather than failing. A payment platform must never block legitimate traffic because the rate limiter is down.

Idempotency (middleware/idempotency.py) -- Idempotency key handling to prevent duplicate payments. If a client sends the same Idempotency-Key header twice, the second request returns the cached response from the first. This is critical for payment systems where network timeouts can cause clients to retry requests that already succeeded.

Phase 5: Services

Two service modules containing business logic that spans multiple routes:

Routing (services/routing.py) -- The intelligent payment routing engine that determines which provider to use for a given payment. Given a country and payment method, it queries the routing table for available providers, orders them by priority, and returns the best match. If the top provider fails, the caller can request the next one in the fallback chain.

Encryption (services/encryption.py) -- Fernet/AES encryption for provider credentials. When a merchant stores their Stripe API key or their Hub2 subscription key in 0fee.dev, that credential is encrypted at rest. The encryption service handles key derivation, encryption, and decryption transparently.

Phase 6: API Routes

Six route modules exposing 30+ endpoints:

GET  /                                     # API info
GET  /health                               # Basic health check
GET  /health/ready                         # Readiness (DB + Cache)
GET  /health/live                          # Liveness

POST /v1/auth/register                     # User registration
POST /v1/auth/login                        # User login
POST /v1/auth/otp/request                  # Request OTP
POST /v1/auth/otp/verify                   # Verify OTP
POST /v1/auth/refresh                      # Refresh token
POST /v1/auth/logout                       # Logout

GET  /v1/apps                              # List apps
POST /v1/apps                              # Create app
GET  /v1/apps/{app_id}                     # Get app
PATCH /v1/apps/{app_id}                    # Update app
GET  /v1/apps/{app_id}/keys               # List API keys
POST /v1/apps/{app_id}/keys               # Create API key
DELETE /v1/apps/{app_id}/keys/{key_id}    # Revoke key
GET  /v1/apps/{app_id}/providers           # List providers
POST /v1/apps/{app_id}/providers           # Add provider
DELETE /v1/apps/{app_id}/providers/{id}    # Remove provider
GET  /v1/apps/{app_id}/routes              # List routes
POST /v1/apps/{app_id}/routes              # Create route
DELETE /v1/apps/{app_id}/routes/{id}       # Delete route

POST /v1/payments                          # Initiate payment
GET  /v1/payments                          # List payments
GET  /v1/payments/{payment_id}             # Get payment
POST /v1/payments/{payment_id}/authenticate # Submit OTP
POST /v1/payments/{payment_id}/cancel      # Cancel payment

GET  /v1/checkout/payment-methods          # Methods for country
POST /v1/checkout/sessions                 # Create session
GET  /v1/checkout/sessions/{session_id}    # Get session
GET  /v1/checkout/countries                # Supported countries

POST /v1/webhooks/{provider_id}            # Provider webhook

Phase 7: Main App and Configuration

The final phase tied everything together. backend/main.py defined the FastAPI application with lifespan events (database initialization on startup, cache cleanup on shutdown), mounted all routers, and configured CORS. A requirements.txt listed all Python dependencies. An .env.example documented every environment variable.

The Complete File Tree

Here is every file created in Session 001:

backend/
├── __init__.py
├── main.py                    # FastAPI entry point
├── config.py                  # Pydantic settings
├── database.py                # SQLite + WAL mode
├── cache.py                   # DragonflyDB client
├── requirements.txt           # Python dependencies
├── .env.example               # Environment template
├── models/
│   ├── __init__.py
│   ├── base.py                # Enums, base responses
│   ├── user.py                # Auth models
│   ├── app.py                 # App/tenant models
│   ├── transaction.py         # Payment models
│   ├── provider.py            # Provider capability models
│   ├── webhook.py             # Webhook event models
│   └── wallet.py              # Wallet models
├── providers/
│   ├── __init__.py
│   ├── base.py                # Abstract base class
│   ├── registry.py            # Provider registry
│   ├── test/
│   │   ├── __init__.py
│   │   └── provider.py        # Test provider (magic amounts)
│   ├── stripe/
│   │   ├── __init__.py
│   │   └── provider.py        # Stripe Checkout Sessions
│   ├── paypal/
│   │   ├── __init__.py
│   │   └── provider.py        # PayPal Orders API
│   ├── hub2/
│   │   ├── __init__.py
│   │   └── provider.py        # Hub2 (Francophone Africa)
│   └── pawapay/
│       ├── __init__.py
│       └── provider.py        # PawaPay (21+ countries)
├── middleware/
│   ├── __init__.py
│   ├── auth.py                # API key authentication
│   ├── rate_limit.py          # Redis-based rate limiting
│   └── idempotency.py         # Duplicate payment prevention
├── services/
│   ├── __init__.py
│   ├── routing.py             # Payment routing engine
│   └── encryption.py          # Credential encryption
└── routes/
    ├── __init__.py
    ├── health.py              # Health checks
    ├── auth.py                # User auth endpoints
    ├── apps.py                # App management
    ├── payments.py            # Payment operations
    ├── checkout.py            # Checkout sessions
    └── webhooks.py            # Provider webhooks

42 files. Zero boilerplate generators. Zero copy-paste from templates. Every file was written from scratch based on the implementation specification.

The Numbers

MetricValue
Total files42
Total lines of code~7,900
API endpoints30+
Payment providers5 (Test, Stripe, PayPal, Hub2, PawaPay)
Database tables15+
Pydantic models40+
Middleware components3
Service modules2
Time elapsed~45 minutes

Why SQLite with WAL Mode?

A payment orchestration platform running on SQLite might raise eyebrows. The reasoning was pragmatic:

  1. Zero configuration. No PostgreSQL server to provision, no connection pooling to configure, no credentials to manage. The database is a single file.
  2. WAL mode handles concurrency. Multiple readers can operate simultaneously while one writer holds the lock. For a platform in its early stages, this is more than sufficient.
  3. Portability. The entire database can be backed up by copying a single file. Moving environments means copying one .db file.
  4. Performance is adequate. SQLite with WAL can handle hundreds of writes per second. A new payment platform does not need to handle thousands of concurrent writes on day one.

The plan always included a migration path to PostgreSQL -- and that migration eventually happened in Session 081. But starting with SQLite meant the backend could be built, tested, and deployed without any external database dependency.

Why DragonflyDB Instead of Redis?

DragonflyDB is a drop-in Redis replacement that uses a multi-threaded architecture instead of Redis's single-threaded event loop. For 0fee.dev, the practical benefits were:

  • Lower memory usage -- DragonflyDB uses up to 80% less memory for the same dataset.
  • Same protocol -- Every Redis client library works unchanged.
  • Better performance under load -- The multi-threaded model scales with CPU cores.

Since the cache was used for sessions, rate limiting, OTP codes, and idempotency keys -- all high-frequency, low-latency operations -- the choice of cache engine mattered. DragonflyDB gave better performance with less memory, at zero code cost.

What Made This Possible

Building a complete payment backend in 45 minutes is not normal. Several factors made it feasible:

A thorough specification. The 14-section implementation plan answered every architectural question before the first line of code was written. There was no ambiguity about what to build.

A clear phase sequence. Each phase built on the previous one. The dependency graph was linear, which meant no backtracking, no circular dependencies, no redesigns mid-session.

Python and FastAPI. Python's expressiveness and FastAPI's decorator-based routing minimize boilerplate. A route handler that would take 30 lines in Java takes 10 in Python.

The adapter pattern. All five payment providers implemented the same interface. Once the base class was defined, each provider was a self-contained module that could be written without touching any other code.

This was Session 001. The backend existed. The next session would add two more providers, a complete SolidJS dashboard, a checkout widget, Celery background tasks, and two SDKs -- in 60 minutes.


This article is part of the "How We Built 0fee.dev" series. 0fee.dev is a payment orchestrator covering 53+ providers across 200+ countries, built by Juste A. GNIMAVO and Claude from Abidjan with zero human engineers. Follow the series for the complete build story.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles