Skip to content

Latest commit

Β 

History

History
1069 lines (879 loc) Β· 38.3 KB

File metadata and controls

1069 lines (879 loc) Β· 38.3 KB

Architecture Deep Dive

This document provides an in-depth explanation of the Bug Bounty Platform's architecture, design decisions, and the reasoning behind technical choices.


Table of Contents

  1. Overview
  2. High-Level Architecture
  3. Backend Architecture
  4. Frontend Architecture
  5. Database Design
  6. Security Architecture
  7. Infrastructure
  8. Design Decisions

Overview

This platform is built using a modern, production-ready architecture that emphasizes:

  1. Separation of Concerns - Clear boundaries between layers
  2. Type Safety - Compile-time guarantees in both backend and frontend
  3. Async-First - Scalable concurrent operations
  4. Security by Design - Multiple defense layers
  5. Developer Experience - Hot reload, linting, type checking
  6. Production Ready - Containerized, migrated, monitored

High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Client Browser                        β”‚
β”‚                  (React + TypeScript SPA)                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ HTTPS
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Nginx (Reverse Proxy)                  β”‚
β”‚  - Serves static React build                                 β”‚
β”‚  - Proxies /api/* to FastAPI backend                         β”‚
β”‚  - Gzip compression + security headers                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                                 β”‚
        β–Ό                                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FastAPI Backend   │◄────────►│  Redis (Cache)     β”‚
β”‚  (Python 3.12+)    β”‚          β”‚  - Sessions        β”‚
β”‚  - REST API        β”‚          β”‚  - Rate limiting   β”‚
β”‚  - JWT Auth        β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  - Business Logic  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL 18     β”‚
β”‚  - Primary data    β”‚
β”‚  - UUID v7 PKs     β”‚
β”‚  - ACID guarantees β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why This Architecture?

Traditional Monolith vs. Microservices:

  • This is a modular monolith - easier to develop, deploy, and debug than microservices
  • Services can be extracted into microservices later if needed
  • Single database = ACID transactions across all entities

Why Nginx?

  • Production-grade reverse proxy
  • Static file serving for React build
  • Gzip compression reduces bandwidth
  • SSL/TLS termination
  • Load balancing (if scaled horizontally)

Why Redis?

  • Fast in-memory cache for session data
  • Rate limiting without hitting PostgreSQL
  • Can be used for pub/sub (WebSockets) if needed later

Backend Architecture

Layered Architecture

The backend follows a strict layered architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Routes (API Layer)                     β”‚
β”‚  - HTTP request/response handling                         β”‚
β”‚  - Input validation (Pydantic schemas)                    β”‚
β”‚  - Dependency injection                                   β”‚
β”‚  - OpenAPI documentation                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ calls
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Services (Business Logic)                β”‚
β”‚  - Core business rules                                    β”‚
β”‚  - Orchestrates multiple repositories                     β”‚
β”‚  - Transaction management                                 β”‚
β”‚  - Domain logic                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ calls
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               Repositories (Data Access)                  β”‚
β”‚  - CRUD operations                                        β”‚
β”‚  - Query building                                         β”‚
β”‚  - Database interaction                                   β”‚
β”‚  - No business logic                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ operates on
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Models (Database Entities)               β”‚
β”‚  - SQLAlchemy ORM models                                  β”‚
β”‚  - Relationships                                          β”‚
β”‚  - Database schema definition                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example: User Login Flow

Let's trace a login request through the layers:

# 1. Route Layer (backend/app/auth/routes.py)
@router.post("/login")
async def login(
    credentials: LoginRequest,
    session: DatabaseSession,
) -> TokenResponse:
    access_token, refresh_token = await authenticate_user(
        session=session,
        email=credentials.email,
        password=credentials.password,
        device_info=credentials.device_info,
    )
    return TokenResponse(
        access_token=access_token,
        refresh_token=refresh_token,
    )

The route handler:

  • Receives HTTP POST request at /api/v1/auth/login
  • Validates input using LoginRequest Pydantic model
  • Injects database session via FastAPI's Depends()
  • Calls service function authenticate_user()
  • Returns response as TokenResponse Pydantic model
# 2. Service Layer (backend/app/auth/service.py)
async def authenticate_user(
    session: AsyncSession,
    email: str,
    password: str,
    device_info: str | None,
) -> tuple[str, str]:
    user_repo = UserRepository(session)

    # Find user by email
    user = await user_repo.find_by_email(email)
    if not user:
        raise InvalidCredentialsError()

    # Verify password (timing-safe comparison)
    if not security.verify_password(password, user.password_hash):
        raise InvalidCredentialsError()

    # Create tokens
    access_token = security.create_access_token(user)
    refresh_token = security.create_refresh_token()

    # Store refresh token in database
    token_repo = RefreshTokenRepository(session)
    await token_repo.create_refresh_token(
        user_id=user.id,
        token=refresh_token,
        device_info=device_info,
    )

    await session.commit()

    return access_token, refresh_token

The service layer:

  • Implements business logic (authentication rules)
  • Coordinates multiple repositories (User + RefreshToken)
  • Handles password verification securely
  • Creates JWT tokens
  • Manages transactions (commit)
  • Throws domain exceptions (InvalidCredentialsError)
# 3. Repository Layer (backend/app/user/repository.py)
class UserRepository(BaseRepository[User]):
    async def find_by_email(self, email: str) -> User | None:
        stmt = select(User).where(User.email == email)
        result = await self.session.execute(stmt)
        return result.scalar_one_or_none()

The repository:

  • Builds SQL query using SQLAlchemy
  • Executes query against database
  • Returns ORM model or None
  • No business logic - pure data access
# 4. Model Layer (backend/app/user/models.py)
class User(Base):
    __tablename__ = "users"

    id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    password_hash: Mapped[str] = mapped_column(String(255))
    full_name: Mapped[str] = mapped_column(String(255))
    role: Mapped[UserRole] = mapped_column(Enum(UserRole))

    # Relationships
    refresh_tokens: Mapped[list["RefreshToken"]] = relationship(
        back_populates="user",
        cascade="all, delete-orphan",
    )

The model:

  • Defines database schema
  • Maps Python classes to database tables
  • Declares relationships between entities
  • Provides type hints for all fields

Why Layers?

Separation of Concerns:

  • Routes don't know about database queries
  • Services don't know about HTTP
  • Repositories don't implement business rules
  • Models are pure data structures

Testability:

  • Mock repositories to test services
  • Mock services to test routes
  • Unit test each layer independently

Maintainability:

  • Change database? Update repositories only
  • Change business logic? Update services only
  • Change API format? Update routes and schemas only

Type Safety:

  • Each layer has strict type annotations
  • MyPy verifies types at compile time
  • Refactoring is safe and predictable

Module Structure

Domain-Driven Design (DDD)

Each domain module is self-contained:

backend/src/app/user/
β”œβ”€β”€ __init__.py
β”œβ”€β”€ models.py          # User database model
β”œβ”€β”€ repository.py      # UserRepository
β”œβ”€β”€ schemas.py         # Pydantic request/response models
β”œβ”€β”€ routes.py          # API endpoints
β”œβ”€β”€ service.py         # Business logic functions
└── exceptions.py      # Domain-specific exceptions

Why this structure?

  • All user-related code is in one place
  • Easy to find relevant files
  • Can be extracted to a separate service later
  • Clear ownership and boundaries

Core Module

backend/src/app/core/
β”œβ”€β”€ Base.py                # Base model classes (UUIDMixin, etc.)
β”œβ”€β”€ base_repository.py     # Generic repository with CRUD
β”œβ”€β”€ database.py            # Database session management
β”œβ”€β”€ security.py            # Password hashing, JWT creation
β”œβ”€β”€ dependencies.py        # FastAPI dependencies (auth, etc.)
β”œβ”€β”€ exceptions.py          # Base exception classes
β”œβ”€β”€ enums.py               # SafeEnum pattern
β”œβ”€β”€ logging.py             # Structured logging
β”œβ”€β”€ rate_limit.py          # Rate limiting configuration
└── constants.py           # String length constraints

The core module provides:

  • Reusable base classes
  • Shared utilities
  • Database connection management
  • Security primitives

Frontend Architecture

Component-Based Architecture

React's component model enables:

  • Reusability: UI elements as self-contained components
  • Composability: Complex UIs from simple pieces
  • Maintainability: Change one component without breaking others
frontend/src/
β”œβ”€β”€ routes/                    # File-based routing
β”‚   β”œβ”€β”€ landing/              # Public landing page
β”‚   β”œβ”€β”€ login/                # Authentication
β”‚   β”œβ”€β”€ dashboard/            # User dashboard
β”‚   β”œβ”€β”€ programs/             # Browse programs
β”‚   β”‚   └── [slug]/          # Dynamic route for program detail
β”‚   β”œβ”€β”€ company/              # Company dashboard (nested routes)
β”‚   β”‚   β”œβ”€β”€ programs/        # Manage programs
β”‚   β”‚   β”œβ”€β”€ inbox/           # Incoming reports
β”‚   β”‚   └── reports/         # Report triage
β”‚   └── admin/                # Admin panel
β”‚
β”œβ”€β”€ components/                # Reusable UI components
β”‚   β”œβ”€β”€ common/               # Shared components (Button, Card, etc.)
β”‚   β”œβ”€β”€ forms/                # Form components
β”‚   └── layouts/              # Layout components (Shell, etc.)
β”‚
β”œβ”€β”€ api/                       # Backend integration
β”‚   β”œβ”€β”€ hooks/                # React Query hooks (useAuth, usePrograms)
β”‚   β”œβ”€β”€ types/                # TypeScript interfaces
β”‚   └── index.ts              # Axios client configuration
β”‚
β”œβ”€β”€ stores/                    # State management (Zustand)
β”‚   β”œβ”€β”€ auth.store.ts         # Authentication state
β”‚   β”œβ”€β”€ shell.ui.store.ts     # UI state (sidebar, modals)
β”‚   └── *.form.store.ts       # Form state
β”‚
└── core/                      # App configuration
    β”œβ”€β”€ app/                  # App setup (router, providers)
    β”œβ”€β”€ styles/               # Global styles
    └── config.ts             # Constants and configuration

State Management Strategy

The frontend uses a hybrid state management approach:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Server State (TanStack Query)               β”‚
β”‚  - API data (users, programs, reports)                   β”‚
β”‚  - Automatic caching and revalidation                    β”‚
β”‚  - Loading/error states                                  β”‚
β”‚  - Optimistic updates                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         +
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Client State (Zustand)                      β”‚
β”‚  - UI state (sidebar open/closed, modals)                β”‚
β”‚  - Form state (program creation, report submission)      β”‚
β”‚  - Authentication tokens (persisted to localStorage)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why two state management libraries?

  1. TanStack Query for server state:

    • Automatic caching (no need to manually cache API responses)
    • Background refetching (keeps data fresh)
    • Loading and error states out of the box
    • Optimistic updates for better UX
    • Pagination and infinite scroll support
  2. Zustand for client state:

    • Simple API (less boilerplate than Redux)
    • TypeScript-first design
    • Persistence support (auth tokens)
    • No provider wrapper needed
    • Minimal re-renders

Example: Fetching programs with TanStack Query

// api/hooks/usePrograms.ts
export const usePrograms = (params?: ProgramQueryParams) => {
  return useQuery({
    queryKey: ["programs", params],
    queryFn: () => api.get<ProgramListResponse>("/programs", { params }),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
};

// In component
function ProgramList() {
  const { data, isLoading, error } = usePrograms({ page: 1, limit: 20 });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      {data.items.map(program => (
        <ProgramCard key={program.id} program={program} />
      ))}
    </div>
  );
}

TanStack Query automatically:

  • Caches the response (keyed by ["programs", params])
  • Shows loading state while fetching
  • Refetches on window focus (configurable)
  • Deduplicates concurrent requests
  • Provides error handling

Example: UI state with Zustand

// stores/shell.ui.store.ts
interface ShellUIStore {
  sidebarOpen: boolean;
  toggleSidebar: () => void;
}

export const useShellUIStore = create<ShellUIStore>((set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set((state) => ({
    sidebarOpen: !state.sidebarOpen
  })),
}));

// In component
function Sidebar() {
  const { sidebarOpen, toggleSidebar } = useShellUIStore();

  return (
    <aside className={sidebarOpen ? "open" : "closed"}>
      <button onClick={toggleSidebar}>Toggle</button>
    </aside>
  );
}

Zustand provides:

  • Simple hook-based API
  • No provider wrapper
  • TypeScript support
  • Minimal re-renders (only components using the changed state)

File-Based Routing

Using React Router 7's file-based routing:

routes/
β”œβ”€β”€ landing/
β”‚   └── page.tsx               # β†’ /
β”œβ”€β”€ login/
β”‚   └── page.tsx               # β†’ /login
β”œβ”€β”€ programs/
β”‚   β”œβ”€β”€ page.tsx               # β†’ /programs
β”‚   └── [slug]/
β”‚       └── page.tsx           # β†’ /programs/:slug
└── company/
    β”œβ”€β”€ layout.tsx             # Shared layout for /company/*
    β”œβ”€β”€ programs/
    β”‚   β”œβ”€β”€ page.tsx           # β†’ /company/programs
    β”‚   └── [id]/
    β”‚       └── page.tsx       # β†’ /company/programs/:id
    └── inbox/
        └── page.tsx           # β†’ /company/inbox

Benefits:

  • Route structure mirrors file structure
  • Automatic code splitting (each route is a separate chunk)
  • Nested layouts (company routes share a layout)
  • Dynamic routes with [param] syntax

Database Design

Schema Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    users     β”‚
│──────────────│
β”‚ id (UUID v7) │◄───┐
β”‚ email        β”‚    β”‚
β”‚ password     β”‚    β”‚
β”‚ role         β”‚    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
                    β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚                            β”‚                β”‚
      β–Ό                            β–Ό                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚refresh_tokens β”‚          β”‚  programs   β”‚  β”‚  reports   β”‚
│───────────────│          │─────────────│  │────────────│
β”‚ id            β”‚          β”‚ id          β”‚  β”‚ id         β”‚
β”‚ user_id (FK)  β”‚          β”‚ owner_id FK β”‚  β”‚ author_id  β”‚
β”‚ token_hash    β”‚          β”‚ name        β”‚  β”‚ program_id β”‚
β”‚ device_info   β”‚          β”‚ slug        β”‚  β”‚ title      β”‚
β”‚ family_id     β”‚          β”‚ status      β”‚  β”‚ severity   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β–Ό                       β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚  assets  β”‚          β”‚ reward_tiers β”‚
                │──────────│          │──────────────│
                β”‚ id       β”‚          β”‚ id           β”‚
                β”‚ program  β”‚          β”‚ program_id   β”‚
                β”‚ type     β”‚          β”‚ severity     β”‚
                β”‚ target   β”‚          β”‚ amount       β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

UUID v7 Primary Keys

Traditional auto-increment IDs have problems:

  • Predictable (security risk - enumerate all records)
  • Not globally unique (can't merge databases)
  • Require database round-trip to generate

UUIDs solve this but have their own issues:

  • UUID v4 is random (bad for database indexing)
  • Not time-sortable (can't ORDER BY id to get chronological order)

UUID v7 is the best of both worlds:

  • Time-sortable (first 48 bits are Unix timestamp in milliseconds)
  • Globally unique (no collisions even across databases)
  • Good for database indexes (lexicographic order = chronological order)
  • Secure (remaining bits are random)
import uuid_utils as uuid

# Generate UUID v7
user_id = uuid.uuid7()  # β†’ 018d3f54-8c3a-7000-a234-56789abcdef0
                        #    ^^^^^^^^^^^^^^^^ ← timestamp
                        #                     ^^^^^^^^^^^^^^^^^^^ ← random

SafeEnum Pattern

Traditional enums in SQLAlchemy store the enum name:

class Status(enum.Enum):
    ACTIVE = "active"
    PAUSED = "paused"

# Database stores: "ACTIVE" (the Python name)

Problem: If you rename the Python enum, the database breaks:

class Status(enum.Enum):
    RUNNING = "active"  # Renamed ACTIVE β†’ RUNNING
    PAUSED = "paused"

# Database still has "ACTIVE", but Python doesn't recognize it!

SafeEnum solution: Store the value, not the name:

class Status(SafeEnum):
    ACTIVE = "active"
    PAUSED = "paused"

# Database stores: "active" (the value)

Now you can safely rename:

class Status(SafeEnum):
    RUNNING = "active"  # Value is still "active"
    PAUSED = "paused"

# Database has "active", Python maps it to Status.RUNNING βœ“

Soft Deletes vs Hard Deletes

Some models use soft deletes (set deleted_at timestamp instead of removing row):

class SoftDeleteMixin:
    deleted_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True),
        nullable=True,
        default=None,
    )

When to use soft deletes:

  • User accounts (compliance requirements - keep audit trail)
  • Financial records (never truly delete)
  • Reports (preserve history even if program is deleted)

When to use hard deletes:

  • Session tokens (no need to keep after logout)
  • Temporary data (caches, OTPs)
  • GDPR deletion requests (must truly delete)

Security Architecture

Multi-Layer Defense

Security is implemented at multiple layers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. Input Validation (Pydantic)                       β”‚
β”‚     - Type checking                                   β”‚
β”‚     - String length limits                            β”‚
β”‚     - Email/URL format validation                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  2. Authentication (JWT)                              β”‚
β”‚     - Token-based auth                                β”‚
β”‚     - Short-lived access tokens (15 min)              β”‚
β”‚     - Long-lived refresh tokens (7 days)              β”‚
β”‚     - Token versioning                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  3. Authorization (RBAC)                              β”‚
β”‚     - Role-based access control                       β”‚
β”‚     - Resource ownership checks                       β”‚
β”‚     - Admin-only endpoints                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  4. Rate Limiting                                     β”‚
β”‚     - 100 req/min default                             β”‚
β”‚     - 20 req/min for auth endpoints                   β”‚
β”‚     - Per-IP tracking                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  5. Secure Storage                                    β”‚
β”‚     - Argon2id password hashing                       β”‚
β”‚     - Hashed refresh tokens (not plaintext)           β”‚
β”‚     - Encrypted secrets in env vars                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

JWT Token Flow

User Login
   β”‚
   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. POST /api/v1/auth/login          β”‚
β”‚     { email, password }              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  2. Verify password (Argon2id)       β”‚
β”‚     - Timing-safe comparison         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  3. Generate tokens                  β”‚
β”‚     - access_token (15 min)          β”‚
β”‚     - refresh_token (7 days)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  4. Store refresh token in DB        β”‚
β”‚     - Hashed (not plaintext)         β”‚
β”‚     - With device info and IP        β”‚
β”‚     - Family ID for replay detection β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  5. Return tokens to client          β”‚
β”‚     { access_token, refresh_token }  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Access token payload:

{
  "sub": "018d3f54-8c3a-7000-a234-56789abcdef0",  // user_id
  "role": "USER",
  "token_version": 1,
  "exp": 1704123456,  // expires in 15 minutes
  "iat": 1704122556
}

Why short-lived access tokens?

  • If stolen, attacker only has 15 minutes
  • No way to revoke access tokens (they're stateless)
  • Must use refresh token to get new access token

Refresh token flow:

Access token expires (15 min)
   β”‚
   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. POST /api/v1/auth/refresh        β”‚
β”‚     { refresh_token }                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  2. Verify refresh token in DB       β”‚
β”‚     - Check hash matches             β”‚
β”‚     - Check not expired              β”‚
β”‚     - Check not revoked              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  3. Token rotation                   β”‚
β”‚     - Delete old refresh token       β”‚
β”‚     - Generate new refresh token     β”‚
β”‚     - Store new token in DB          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  4. Generate new access token        β”‚
β”‚     - Same user_id                   β”‚
β”‚     - Incremented token_version      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  5. Return new tokens                β”‚
β”‚     { access_token, refresh_token }  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Token rotation prevents replay attacks:

  • Each refresh token is single-use
  • If an attacker steals a refresh token, it becomes invalid after one use
  • If a refresh token is reused, we detect it (family ID mismatch) and revoke all tokens for that user

Token Versioning

Token versioning allows instant invalidation:

class User(Base):
    token_version: Mapped[int] = mapped_column(Integer, default=1)

# When user changes password:
user.token_version += 1
await session.commit()

Now all existing access tokens become invalid:

  • Old tokens have token_version: 1
  • User's current token_version is 2
  • Token verification fails: 1 != 2

No need to maintain a token blacklist!


Infrastructure

Docker Compose Architecture

Production (compose.yml):

services:
  nginx:
    build: ./infra/nginx
    ports:
      - "${NGINX_HOST_PORT}:80"
    depends_on:
      - backend

  backend:
    build: ./backend
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - db
      - redis

  db:
    image: postgres:18-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

Why this structure?

  • nginx depends on backend: Nginx can't start until backend is ready
  • backend depends on db + redis: Backend needs database and cache
  • Volumes for data persistence: Database and Redis data survives container restarts

Multi-Stage Docker Builds

Backend Dockerfile:

# Stage 1: Build dependencies
FROM python:3.12-slim as builder
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir -e .

# Stage 2: Production image
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY ./src ./src
CMD ["gunicorn", "main:app"]

Benefits:

  • Smaller final image (no build tools)
  • Faster builds (dependencies cached in builder stage)
  • More secure (no build dependencies in production)

Frontend Dockerfile (Nginx):

# Stage 1: Build React app
FROM node:22-alpine as builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
RUN pnpm build

# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

Benefits:

  • No Node.js in production image
  • Nginx is optimized for serving static files
  • Smaller final image (~50MB vs ~500MB)

Design Decisions

Why FastAPI?

Compared to Flask:

  • Async/await support (FastAPI wins)
  • Automatic OpenAPI docs (FastAPI wins)
  • Data validation built-in with Pydantic (FastAPI wins)
  • Type hints for IDE autocomplete (FastAPI wins)

Compared to Django:

  • Async ORM support (tie - Django 4.1+ has it)
  • Flexibility (FastAPI wins - Django is opinionated)
  • Admin panel (Django wins)
  • Batteries included (Django wins - has auth, admin, etc.)

Decision: FastAPI for its async-first design and modern Python features.

Why PostgreSQL?

Compared to MySQL:

  • JSON support (tie)
  • Full-text search (PostgreSQL wins)
  • ACID compliance (tie)
  • JSON indexes (PostgreSQL wins)

Compared to MongoDB:

  • ACID transactions (PostgreSQL wins)
  • Schema validation (tie - Postgres has JSON schema)
  • Joins (PostgreSQL wins)
  • Flexibility (MongoDB wins for unstructured data)

Decision: PostgreSQL for ACID guarantees and relational data modeling.

Why React + TypeScript?

Compared to Vue:

  • Ecosystem size (React wins)
  • TypeScript support (tie)
  • Learning curve (Vue wins - easier)
  • Corporate backing (tie - React by Meta, Vue independent)

Compared to Svelte:

  • Maturity (React wins)
  • Job market (React wins)
  • Bundle size (Svelte wins)
  • Learning curve (Svelte wins)

Decision: React + TypeScript for its mature ecosystem and industry adoption.

Why Zustand?

Compared to Redux:

  • Boilerplate (Zustand wins - much simpler)
  • DevTools (Redux wins - more mature)
  • Middleware (tie)
  • Bundle size (Zustand wins)

Compared to Context API:

  • Performance (Zustand wins - no unnecessary re-renders)
  • Persistence (Zustand wins - built-in)
  • DX (Zustand wins - simpler API)

Decision: Zustand for its simplicity and performance.

Why TanStack Query?

Compared to Redux Toolkit Query:

  • Flexibility (TanStack wins - not tied to Redux)
  • Cache management (tie)
  • Bundle size (TanStack wins)
  • Learning curve (tie)

Compared to SWR:

  • Features (TanStack wins - more complete)
  • Bundle size (SWR wins)
  • Pagination (TanStack wins)
  • Community (tie)

Decision: TanStack Query for its comprehensive feature set and framework-agnostic design.


Performance Considerations

Database Indexing

Indexes are created for frequently queried columns:

class User(Base):
    email: Mapped[str] = mapped_column(
        String(255),
        unique=True,
        index=True  # ← Index for fast lookups
    )

Trade-off:

  • Faster reads (queries using email are fast)
  • Slower writes (index must be updated on insert/update)
  • More disk space (index is stored separately)

Connection Pooling

Database connections are expensive to create. Connection pooling reuses connections:

DB_POOL_SIZE=20          # Max connections in pool
DB_MAX_OVERFLOW=10       # Extra connections when pool is full
DB_POOL_TIMEOUT=30       # Wait 30s for available connection
DB_POOL_RECYCLE=1800     # Recycle connections after 30min

How it works:

  1. Request arrives
  2. Backend grabs connection from pool
  3. Executes query
  4. Returns connection to pool (doesn't close it)
  5. Next request reuses same connection

Benefits:

  • Faster request handling (no connection overhead)
  • Reduced database load (fewer connection negotiations)
  • Better scalability (handle more concurrent requests)

Redis Caching

Redis is used for frequently accessed data:

# Without cache
user = await user_repo.find_by_id(user_id)  # Database query every time

# With cache
user = await redis.get(f"user:{user_id}")
if not user:
    user = await user_repo.find_by_id(user_id)
    await redis.set(f"user:{user_id}", user, ex=300)  # Cache for 5 min

Cache invalidation strategies:

  1. Time-based (TTL) - expire after N seconds
  2. Event-based - invalidate on update/delete
  3. LRU (Least Recently Used) - Redis automatically evicts old keys

Frontend Code Splitting

Vite automatically splits code by route:

// routes/dashboard/page.tsx is lazy-loaded
const Dashboard = lazy(() => import('./routes/dashboard/page'));

Benefits:

  • Faster initial page load (only load landing page)
  • Subsequent navigation is fast (chunks are cached)
  • Better bandwidth usage (don't load admin panel for regular users)

Conclusion

This architecture prioritizes:

  1. Developer Experience - Hot reload, type safety, linting
  2. Security - Multiple defense layers, modern auth patterns
  3. Scalability - Async operations, connection pooling, caching
  4. Maintainability - Clear layer separation, domain-driven design
  5. Production Readiness - Containerization, migrations, monitoring

Every design decision has trade-offs. This architecture favors correctness, security, and maintainability over raw performance (which can be optimized later if needed).

For more details on specific topics: