# Getting Started: Build Your Own Bug Bounty Platform This guide walks you through building a similar platform from scratch, explaining each step and the reasoning behind design decisions. --- ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Project Setup](#project-setup) 3. [Backend Development](#backend-development) 4. [Frontend Development](#frontend-development) 5. [Database Design](#database-design) 6. [Authentication](#authentication) 7. [API Development](#api-development) 8. [Testing](#testing) 9. [Deployment](#deployment) --- ## Prerequisites Before starting, you should understand: **Python:** - Async/await (`async def`, `await`) - Type hints (`def func(x: int) -> str`) - Context managers (`async with`) - Decorators (`@router.get()`) **TypeScript/JavaScript:** - Promises and async/await - React hooks (`useState`, `useEffect`) - TypeScript types and interfaces - ES6+ syntax (arrow functions, destructuring) **Database:** - SQL basics (SELECT, INSERT, UPDATE, DELETE) - Relationships (one-to-many, many-to-many) - Indexes and foreign keys **Tools:** - Git (version control) - Docker (containerization) - Command line basics --- ## Project Setup ### Step 1: Create Project Structure ```bash mkdir bug-bounty-platform cd bug-bounty-platform mkdir -p backend/src/app mkdir -p frontend/src mkdir -p infra/nginx mkdir -p infra/docker ``` ### Step 2: Initialize Backend (Python) ```bash cd backend touch pyproject.toml ``` **pyproject.toml:** ```toml [project] name = "bug-bounty-backend" version = "1.0.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.123.0", "uvicorn[standard]>=0.40.0", "sqlalchemy>=2.0.0", "asyncpg>=0.30.0", "alembic>=1.15.0", "pydantic>=2.0.0", "pydantic-settings>=2.7.0", "pyjwt>=2.10.0", "pwdlib[argon2]>=0.2.0", "uuid-utils>=0.10.0", "python-multipart>=0.0.20", ] [project.optional-dependencies] dev = [ "ruff>=0.9.0", "mypy>=1.15.0", "pytest>=8.0.0", "pytest-asyncio>=0.25.0", ] [build-system] requires = ["setuptools>=75.0.0"] build-backend = "setuptools.build_meta" ``` **Why these dependencies?** - `fastapi` - Modern async web framework - `sqlalchemy` - ORM for database operations - `asyncpg` - Fast async PostgreSQL driver - `alembic` - Database migrations - `pydantic` - Data validation - `pyjwt` - JWT token handling - `pwdlib[argon2]` - Secure password hashing - `uuid-utils` - UUID v7 support ### Step 3: Initialize Frontend (React + TypeScript) ```bash cd ../frontend pnpm create vite@latest . --template react-ts pnpm install ``` **Install additional dependencies:** ```bash pnpm add react-router-dom @tanstack/react-query zustand axios zod pnpm add -D sass stylelint @biomejs/biome ``` **Why these dependencies?** - `react-router-dom` - File-based routing - `@tanstack/react-query` - Server state management - `zustand` - Client state management - `axios` - HTTP client - `zod` - Runtime validation - `sass` - CSS preprocessing - `biome` - Linting and formatting --- ## Backend Development ### Step 1: Create Base Configuration **backend/src/config.py:** ```python from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", ) APP_NAME: str = "Bug Bounty Platform" ENVIRONMENT: str = "development" DEBUG: bool = True DATABASE_URL: str SECRET_KEY: str JWT_ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 15 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 CORS_ORIGINS: list[str] = ["http://localhost:3000"] settings = Settings() ``` **Why Pydantic Settings?** - Type validation (DATABASE_URL must be a string) - Environment variable loading (reads from .env) - Default values - IDE autocomplete ### Step 2: Set Up Database **backend/src/app/core/database.py:** ```python from sqlalchemy.ext.asyncio import ( create_async_engine, AsyncSession, async_sessionmaker, ) from typing import AsyncGenerator class DatabaseSessionManager: def __init__(self): self._engine = None self._sessionmaker = None def init(self, database_url: str): self._engine = create_async_engine( database_url, echo=True, # Log SQL queries (disable in production) pool_size=20, max_overflow=10, ) self._sessionmaker = async_sessionmaker( self._engine, class_=AsyncSession, expire_on_commit=False, ) async def session(self) -> AsyncGenerator[AsyncSession, None]: if self._sessionmaker is None: raise RuntimeError("Database not initialized") async with self._sessionmaker() as session: try: yield session except Exception: await session.rollback() raise finally: await session.close() db_manager = DatabaseSessionManager() ``` **Key concepts:** - `AsyncSession` - Async database session - `async_sessionmaker` - Factory for creating sessions - Context manager (`async with`) - Automatically commits/rolls back - Connection pooling - Reuse connections ### Step 3: Create Base Models **backend/src/app/core/Base.py:** ```python from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy import DateTime, func from uuid import UUID from datetime import datetime import uuid_utils as uuid class Base(DeclarativeBase): pass class UUIDMixin: id: Mapped[UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid7, ) class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), ) ``` **Why mixins?** - DRY (Don't Repeat Yourself) - Every model gets `id`, `created_at`, `updated_at` automatically - Can add more mixins (SoftDeleteMixin, etc.) ### Step 4: Create User Model **backend/src/app/user/models.py:** ```python from sqlalchemy import String, Enum from sqlalchemy.orm import Mapped, mapped_column from app.core.Base import Base, UUIDMixin, TimestampMixin from enum import StrEnum class UserRole(StrEnum): USER = "user" COMPANY = "company" ADMIN = "admin" class User(Base, UUIDMixin, TimestampMixin): __tablename__ = "users" 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, values_callable=lambda obj: [e.value for e in obj]), default=UserRole.USER, ) ``` **Key points:** - `Mapped[str]` - Type hint for SQLAlchemy - `unique=True, index=True` - Fast lookups, prevent duplicates - `Enum` with values_callable - Store enum value, not name (SafeEnum pattern) ### Step 5: Create User Repository **backend/src/app/user/repository.py:** ```python from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.user.models import User from uuid import UUID class UserRepository: def __init__(self, session: AsyncSession): self.session = session async def get_by_id(self, user_id: UUID) -> User | None: stmt = select(User).where(User.id == user_id) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_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() async def create( self, email: str, password_hash: str, full_name: str, role: str = "user", ) -> User: user = User( email=email, password_hash=password_hash, full_name=full_name, role=role, ) self.session.add(user) await self.session.flush() await self.session.refresh(user) return user ``` **Why repositories?** - Separate data access from business logic - Reusable queries - Easy to test (mock repository) ### Step 6: Create User Schemas **backend/src/app/user/schemas.py:** ```python from pydantic import BaseModel, EmailStr, Field from uuid import UUID from datetime import datetime class UserCreate(BaseModel): email: EmailStr password: str = Field(..., min_length=8) full_name: str = Field(..., min_length=1, max_length=255) class UserResponse(BaseModel): id: UUID email: str full_name: str role: str created_at: datetime model_config = {"from_attributes": True} ``` **Why Pydantic schemas?** - Input validation (email format, password length) - Automatic OpenAPI docs - Type safety - Serialization (ORM model → JSON) ### Step 7: Create User Routes **backend/src/app/user/routes.py:** ```python from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import db_manager from app.user.schemas import UserResponse from app.user.repository import UserRepository from app.core.dependencies import get_current_user from typing import Annotated router = APIRouter() DatabaseSession = Annotated[ AsyncSession, Depends(db_manager.session) ] CurrentUser = Annotated[User, Depends(get_current_user)] @router.get("/me", response_model=UserResponse) async def get_me(user: CurrentUser) -> UserResponse: return UserResponse.from_orm(user) ``` **Key concepts:** - `Annotated` - Type alias with dependency - `Depends()` - Dependency injection - `response_model` - Automatic serialization - Route handler returns Python object, FastAPI converts to JSON --- ## Authentication ### Step 1: Password Hashing **backend/src/app/core/security.py:** ```python from pwdlib import PasswordHash from pwdlib.hashers.argon2 import Argon2Hasher password_hash = PasswordHash((Argon2Hasher(),)) def hash_password(password: str) -> str: return password_hash.hash(password) def verify_password(password: str, hash: str) -> bool: try: return password_hash.verify(password, hash) except Exception: return False ``` **Why Argon2?** - Winner of Password Hashing Competition (2015) - Resistant to GPU cracking - Memory-hard (expensive to parallelize) ### Step 2: JWT Token Creation **backend/src/app/core/security.py (continued):** ```python import jwt from datetime import datetime, timedelta from uuid import UUID from config import settings def create_access_token(user: User) -> str: payload = { "sub": str(user.id), "role": user.role, "token_version": user.token_version, "exp": datetime.utcnow() + timedelta( minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES ), "iat": datetime.utcnow(), } return jwt.encode( payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM, ) def decode_access_token(token: str) -> dict: try: return jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM], ) except jwt.ExpiredSignatureError: raise UnauthorizedError("Token expired") except jwt.InvalidTokenError: raise UnauthorizedError("Invalid token") ``` **JWT payload:** - `sub` (subject) - User ID - `role` - User role (for authorization) - `token_version` - For instant invalidation - `exp` (expiration) - Token expires in 15 minutes - `iat` (issued at) - Timestamp of creation ### Step 3: Authentication Dependency **backend/src/app/core/dependencies.py:** ```python from fastapi import Header, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import decode_access_token from app.user.repository import UserRepository from app.user.models import User from uuid import UUID async def get_current_user( authorization: str = Header(...), session: AsyncSession = Depends(db_manager.session), ) -> User: if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Invalid token") token = authorization.replace("Bearer ", "") try: payload = decode_access_token(token) except Exception: raise HTTPException(status_code=401, detail="Invalid token") user_id = UUID(payload["sub"]) repo = UserRepository(session) user = await repo.get_by_id(user_id) if not user: raise HTTPException(status_code=401, detail="User not found") if user.token_version != payload["token_version"]: raise HTTPException(status_code=401, detail="Token invalidated") return user ``` **How it works:** 1. Extract `Authorization: Bearer ` header 2. Decode JWT token 3. Get user ID from token payload 4. Query database for user 5. Verify token version matches (for instant invalidation) 6. Return authenticated user --- ## Frontend Development ### Step 1: Create API Client **frontend/src/api/index.ts:** ```typescript import axios from 'axios'; const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json', }, }); api.interceptors.request.use((config) => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { const refreshToken = localStorage.getItem('refresh_token'); if (refreshToken) { try { const { data } = await axios.post('/api/auth/refresh', { refresh_token: refreshToken, }); localStorage.setItem('access_token', data.access_token); localStorage.setItem('refresh_token', data.refresh_token); error.config.headers.Authorization = `Bearer ${data.access_token}`; return axios(error.config); } catch { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); window.location.href = '/login'; } } } return Promise.reject(error); } ); export default api; ``` **What this does:** - Automatically adds `Authorization` header to all requests - Automatically refreshes expired access tokens - Redirects to login if refresh fails ### Step 2: Create Auth Store (Zustand) **frontend/src/stores/auth.store.ts:** ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface AuthStore { accessToken: string | null; refreshToken: string | null; user: User | null; setTokens: (access: string, refresh: string) => void; setUser: (user: User) => void; logout: () => void; } export const useAuthStore = create()( persist( (set) => ({ accessToken: null, refreshToken: null, user: null, setTokens: (access, refresh) => set({ accessToken: access, refreshToken: refresh, }), setUser: (user) => set({ user }), logout: () => set({ accessToken: null, refreshToken: null, user: null, }), }), { name: 'auth-storage', } ) ); ``` **Why Zustand?** - Simple API (no boilerplate) - Built-in persistence (saves to localStorage) - TypeScript support - No provider wrapper needed ### Step 3: Create React Query Hook **frontend/src/api/hooks/useAuth.ts:** ```typescript import { useMutation } from '@tanstack/react-query'; import api from '../index'; import { useAuthStore } from '../../stores/auth.store'; interface LoginRequest { email: string; password: string; } interface LoginResponse { access_token: string; refresh_token: string; } export const useLogin = () => { const setTokens = useAuthStore((state) => state.setTokens); return useMutation({ mutationFn: async (data: LoginRequest) => { const response = await api.post('/auth/login', data); return response.data; }, onSuccess: (data) => { setTokens(data.access_token, data.refresh_token); localStorage.setItem('access_token', data.access_token); localStorage.setItem('refresh_token', data.refresh_token); }, }); }; ``` **Usage in component:** ```typescript function LoginForm() { const login = useLogin(); const handleSubmit = (e: FormEvent) => { e.preventDefault(); login.mutate({ email: 'user@example.com', password: 'password123', }); }; return (
{/* ... */} {login.isError &&

Login failed

}
); } ``` **Why TanStack Query?** - Automatic loading/error states (`isPending`, `isError`) - Optimistic updates - Retry logic - Cache management --- ## Testing ### Backend Testing **backend/tests/test_user.py:** ```python import pytest from httpx import AsyncClient from app.factory import create_app @pytest.fixture async def client(): app = create_app() async with AsyncClient(app=app, base_url="http://test") as client: yield client @pytest.mark.asyncio async def test_register_user(client: AsyncClient): response = await client.post("/api/auth/register", json={ "email": "test@example.com", "password": "password123", "full_name": "Test User", }) assert response.status_code == 201 data = response.json() assert data["email"] == "test@example.com" assert "id" in data ``` **Run tests:** ```bash pytest backend/tests/ ``` ### Frontend Testing **frontend/src/components/LoginForm.test.tsx:** ```typescript import { render, screen, fireEvent } from '@testing-library/react'; import { LoginForm } from './LoginForm'; test('renders login form', () => { render(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); }); test('submits form with credentials', () => { const onSubmit = jest.fn(); render(); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'test@example.com' }, }); fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' }, }); fireEvent.click(screen.getByRole('button', { name: /login/i })); expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123', }); }); ``` --- ## Deployment ### Step 1: Create Docker Compose **compose.yml:** ```yaml version: '3.8' services: nginx: build: ./infra/nginx ports: - "8420:80" depends_on: - backend backend: build: ./backend environment: - DATABASE_URL=${DATABASE_URL} - SECRET_KEY=${SECRET_KEY} depends_on: - db db: image: postgres:18-alpine environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: ``` ### Step 2: Deploy ```bash cp .env.example .env just up ``` --- ## Next Steps Now that you understand the basics: 1. **Read the architecture docs:** [ARCHITECTURE.md](./ARCHITECTURE.md) 2. **Learn design patterns:** [PATTERNS.md](./PATTERNS.md) 3. **Understand the database:** [DATABASE.md](./DATABASE.md) 4. **Study security features:** [SECURITY.md](./SECURITY.md) --- ## Common Pitfalls **1. Not using async/await properly:** ```python async def bad(): user_repo.get_by_id(user_id) # Missing await! async def good(): user = await user_repo.get_by_id(user_id) ``` **2. Mixing sync and async code:** ```python async def bad(): users = session.query(User).all() # Sync query in async function async def good(): stmt = select(User) result = await session.execute(stmt) users = result.scalars().all() ``` **3. Not handling errors:** ```python def bad(): user = await repo.get_by_id(user_id) return user.email # What if user is None? def good(): user = await repo.get_by_id(user_id) if not user: raise UserNotFoundError() return user.email ``` **4. Storing passwords in plaintext:** ```python def bad(): user = User(email=email, password=password) # Plaintext password! def good(): user = User(email=email, password_hash=hash_password(password)) ``` --- ## Resources **Official Documentation:** - [FastAPI](https://fastapi.tiangolo.com/) - [SQLAlchemy](https://docs.sqlalchemy.org/) - [React](https://react.dev/) - [TanStack Query](https://tanstack.com/query/latest) **Books:** - "Clean Architecture" by Robert C. Martin - "Domain-Driven Design" by Eric Evans - "Designing Data-Intensive Applications" by Martin Kleppmann **This Codebase:** - Explore the code in `/backend/src/app/` - Read the other learning docs in `/learn/` - Experiment by modifying the code --- Good luck building your own platform!