This document provides an in-depth explanation of the Bug Bounty Platform's architecture, design decisions, and the reasoning behind technical choices.
- Overview
- High-Level Architecture
- Backend Architecture
- Frontend Architecture
- Database Design
- Security Architecture
- Infrastructure
- Design Decisions
This platform is built using a modern, production-ready architecture that emphasizes:
- Separation of Concerns - Clear boundaries between layers
- Type Safety - Compile-time guarantees in both backend and frontend
- Async-First - Scalable concurrent operations
- Security by Design - Multiple defense layers
- Developer Experience - Hot reload, linting, type checking
- Production Ready - Containerized, migrated, monitored
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
ββββββββββββββββββββββ
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
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 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
LoginRequestPydantic model - Injects database session via FastAPI's
Depends() - Calls service function
authenticate_user() - Returns response as
TokenResponsePydantic 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_tokenThe 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
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
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
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
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
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?
-
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
-
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)
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
ββββββββββββββββ
β 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 β
ββββββββββββ ββββββββββββββββ
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
# ^^^^^^^^^^^^^^^^^^^ β randomTraditional 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 β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 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 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 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_versionis2 - Token verification fails:
1 != 2
No need to maintain a token blacklist!
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:/dataWhy 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
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.confBenefits:
- No Node.js in production image
- Nginx is optimized for serving static files
- Smaller final image (~50MB vs ~500MB)
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.
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.
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.
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.
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.
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)
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 30minHow it works:
- Request arrives
- Backend grabs connection from pool
- Executes query
- Returns connection to pool (doesn't close it)
- 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 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 minCache invalidation strategies:
- Time-based (TTL) - expire after N seconds
- Event-based - invalidate on update/delete
- LRU (Least Recently Used) - Redis automatically evicts old keys
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)
This architecture prioritizes:
- Developer Experience - Hot reload, type safety, linting
- Security - Multiple defense layers, modern auth patterns
- Scalability - Async operations, connection pooling, caching
- Maintainability - Clear layer separation, domain-driven design
- 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:
- Design patterns: PATTERNS.md
- Database schema: DATABASE.md
- Security implementation: SECURITY.md
- Step-by-step tutorial: GETTING-STARTED.md