# B0 — Backend Project Overview & API Conventions **Module:** B0 **Depends on:** Nothing (foundation document) **Read before:** All other backend modules --- ## 1. What We're Building The LynkedUpPro frontend is a fully-functional React 19 SPA that currently runs entirely on a client-side mock data layer (`src/data/mockStore.jsx`). There is no real server, no database, and no persistent state. This backend will replace that mock layer with a production-grade API server. The frontend **does not need to be rewritten** — the Integration Team will replace mock calls with real API calls, component by component. ### Topology ``` ┌─────────────────────────────┐ ┌────────────────────────────────┐ │ Vercel (Frontend) │ │ Backend Host (Railway etc.) │ │ │ │ │ │ React 19 SPA │ HTTPS │ FastAPI (Python) │ │ └─ AuthContext.jsx │◄──────►│ └─ /api/v1/... │ │ └─ mockStore.jsx ──────────┼──────► │ └─ PostgreSQL (via SQLAlchemy)│ │ (will be replaced) │ │ └─ Alembic (migrations) │ └─────────────────────────────┘ └────────────────────────────────┘ │ ┌───────────┴───────────┐ │ External Services │ │ - Groq API (chatbot) │ │ - SMTP (email alerts) │ │ - AWS S3 (file upload)│ └───────────────────────┘ ``` --- ## 2. Technology Stack | Layer | Technology | Version | Reason | |-------|------------|---------|--------| | Language | Python | 3.12+ | Type hints, async support, ecosystem | | Framework | FastAPI | 0.115+ | Async, auto OpenAPI docs, Pydantic v2 | | ORM | SQLAlchemy | 2.0 (async) | Native async, type-safe, PostgreSQL support | | Migrations | Alembic | 1.13+ | Paired with SQLAlchemy, revision history | | Database | PostgreSQL | 16+ | JSONB, full-text search, PostGIS optional | | Auth | python-jose + passlib | latest | JWT tokens + bcrypt hashing | | Validation | Pydantic v2 | 2.x | Request/response schemas, coercion | | HTTP Client | httpx | 0.27+ | Async requests to Groq API | | File Storage | boto3 (AWS S3) | latest | COI/W9/document uploads | | Email | smtplib / sendgrid-python | latest | Compliance alerts, meeting reminders | | Testing | pytest + pytest-asyncio | latest | Async test support | | Linting | ruff + mypy | latest | Fast linting + static type checking | --- ## 3. Backend Project Structure ``` lynkeduppro-api/ ├── alembic/ │ ├── env.py │ ├── script.py.mako │ └── versions/ # One file per migration ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI app instantiation, middleware, CORS │ ├── config.py # Settings via pydantic-settings (reads .env) │ ├── database.py # SQLAlchemy async engine + session factory │ ├── dependencies.py # Shared FastAPI Depends() (get_db, get_current_user, require_role) │ │ │ ├── models/ # SQLAlchemy ORM models (one file per entity) │ │ ├── __init__.py │ │ ├── user.py │ │ ├── property.py │ │ ├── meeting.py │ │ ├── vendor.py │ │ ├── document.py │ │ ├── invoice.py │ │ ├── project.py │ │ ├── compliance.py │ │ └── audit_log.py │ │ │ ├── schemas/ # Pydantic request/response schemas │ │ ├── __init__.py │ │ ├── user.py │ │ ├── property.py │ │ ├── meeting.py │ │ ├── vendor.py │ │ ├── document.py │ │ ├── invoice.py │ │ ├── project.py │ │ └── auth.py │ │ │ ├── routers/ # FastAPI routers (one file per module) │ │ ├── __init__.py │ │ ├── auth.py # /api/v1/auth/* │ │ ├── users.py # /api/v1/users/* │ │ ├── properties.py # /api/v1/properties/* │ │ ├── meetings.py # /api/v1/meetings/* │ │ ├── vendors.py # /api/v1/vendors/* │ │ ├── documents.py # /api/v1/documents/* │ │ ├── invoices.py # /api/v1/invoices/* │ │ ├── projects.py # /api/v1/projects/* │ │ └── chatbot.py # /api/v1/chatbot/* │ │ │ ├── services/ # Business logic (no direct DB calls, no HTTP) │ │ ├── auth_service.py │ │ ├── property_service.py │ │ ├── meeting_service.py │ │ ├── vendor_service.py │ │ ├── notification_service.py │ │ └── chatbot_service.py │ │ │ └── utils/ │ ├── security.py # JWT encode/decode, bcrypt helpers │ ├── permissions.py # Role-based permission checks │ └── mask.py # Sensitive data masking (SSN, bank, EIN) │ ├── tests/ │ ├── conftest.py # Test DB setup, auth fixtures │ ├── test_auth.py │ ├── test_properties.py │ ├── test_meetings.py │ └── ... │ ├── .env # Local secrets (never committed) ├── .env.example # Committed template (no real values) ├── alembic.ini ├── pyproject.toml # Project metadata + dependencies (uv or poetry) └── README.md ``` --- ## 4. API Conventions ### Base URL | Environment | Base URL | |-------------|----------| | Development | `http://localhost:8000/api/v1` | | Production | `https://api.lynkeduppro.com/api/v1` | ### URL Pattern ``` /api/v1/{resource}/{id?}/{sub-resource?} Examples: GET /api/v1/properties ← list GET /api/v1/properties/42 ← single item POST /api/v1/properties ← create PATCH /api/v1/properties/42 ← partial update DELETE /api/v1/properties/42 ← delete GET /api/v1/properties/42/meetings ← nested resource ``` ### HTTP Methods | Method | Use Case | |--------|----------| | `GET` | Read (no side effects) | | `POST` | Create new resource | | `PATCH` | Partial update (preferred over PUT) | | `DELETE` | Soft delete (sets `deleted_at`, never hard deletes) | ### Response Shape **Success (single object):** ```json { "data": { ... }, "meta": null } ``` **Success (paginated list):** ```json { "data": [ ... ], "meta": { "total": 142, "page": 1, "per_page": 25, "pages": 6 } } ``` **Error:** ```json { "detail": "You do not have permission to view this resource.", "code": "FORBIDDEN" } ``` **Error codes used across the API:** | Code | HTTP Status | Meaning | |------|-------------|---------| | `UNAUTHORIZED` | 401 | No valid token | | `FORBIDDEN` | 403 | Valid token, wrong role | | `NOT_FOUND` | 404 | Resource doesn't exist | | `VALIDATION_ERROR` | 422 | Request body fails Pydantic validation | | `CONFLICT` | 409 | Duplicate (e.g., email already registered) | | `RATE_LIMITED` | 429 | Too many requests | | `INTERNAL_ERROR` | 500 | Unexpected server error | ### Pagination All list endpoints support: ``` GET /api/v1/properties?page=1&per_page=25&sort=created_at&order=desc ``` | Param | Default | Max | Description | |-------|---------|-----|-------------| | `page` | 1 | — | Page number (1-indexed) | | `per_page` | 25 | 100 | Results per page | | `sort` | `created_at` | — | Field to sort by | | `order` | `desc` | — | `asc` or `desc` | ### Filtering Filters are passed as query params with double-underscore notation: ``` GET /api/v1/properties?status__eq=Hot+Lead&year_built__lt=2000 GET /api/v1/meetings?agent_id__eq=e1&date__gte=2026-02-01 ``` ### Money Convention All monetary values travel as **integers in cents** between backend and frontend. ```python # Backend stores and returns: 450000 (cents) # Frontend displays: $4,500.00 ``` The frontend divides by 100 for display. This avoids all floating-point precision issues. ### Date/Time Convention All timestamps are **ISO 8601 UTC** strings: ```json "created_at": "2026-02-24T14:30:00Z" "date_of_loss": "2025-11-15" ``` Date-only fields (no time component) use `YYYY-MM-DD`. --- ## 5. Authentication Overview *(Full detail in `01_authentication_module.md`)* - **Mechanism:** JWT Bearer tokens - **Access token lifetime:** 15 minutes - **Refresh token lifetime:** 7 days - **Storage:** `httpOnly` cookie (preferred for XSS safety) with `SameSite=Strict` - **Header (alternative):** `Authorization: Bearer ` Every protected endpoint uses the `get_current_user` dependency: ```python # app/dependencies.py async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User: ... async def require_role(*roles: str): async def checker(current_user: User = Depends(get_current_user)): if current_user.role not in roles: raise HTTPException(status_code=403, detail="Forbidden", headers={"WWW-Authenticate": "Bearer"}) return current_user return checker ``` Usage in a router: ```python @router.get("/admin/stats") async def get_admin_stats( current_user: User = Depends(require_role("ADMIN", "OWNER")) ): ... ``` --- ## 6. CORS Configuration The frontend is hosted on `https://lynkeduppro-crm.vercel.app`. CORS must allow: - Credentials (for cookie-based auth) - All standard methods - `Authorization` and `Content-Type` headers ```python # app/main.py from fastapi.middleware.cors import CORSMiddleware ALLOWED_ORIGINS = [ "http://localhost:5173", # Vite dev server "https://lynkeduppro-crm.vercel.app", # Production frontend ] app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, # Required for httpOnly cookies allow_methods=["*"], allow_headers=["Authorization", "Content-Type", "X-Request-ID"], expose_headers=["X-Total-Count"], ) ``` **Important:** `allow_credentials=True` cannot be combined with `allow_origins=["*"]`. The origins list must be explicit. --- ## 7. Environment Variables **`.env.example`** (commit this; never commit `.env`): ```dotenv # Database DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/lynkeduppro # JWT SECRET_KEY=CHANGE_ME_generate_with_openssl_rand_hex_32 ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=15 REFRESH_TOKEN_EXPIRE_DAYS=7 # Groq AI GROQ_API_KEY=gsk_... # AWS S3 (document storage) AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_S3_BUCKET=lynkeduppro-docs AWS_REGION=us-east-1 # Email (SendGrid) SENDGRID_API_KEY=SG... EMAIL_FROM=noreply@lynkeduppro.com # App ENVIRONMENT=development # development | staging | production DEBUG=true BACKEND_CORS_ORIGINS=http://localhost:5173,https://lynkeduppro-crm.vercel.app ``` Loaded in FastAPI via `pydantic-settings`: ```python # app/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): database_url: str secret_key: str algorithm: str = "HS256" access_token_expire_minutes: int = 15 refresh_token_expire_days: int = 7 groq_api_key: str environment: str = "development" debug: bool = False backend_cors_origins: list[str] = [] class Config: env_file = ".env" settings = Settings() ``` --- ## 8. Database Setup ```bash # Install dependencies pip install -r requirements.txt # or: uv sync # Create database createdb lynkeduppro # Run migrations alembic upgrade head # Seed development data python -m app.scripts.seed_dev ``` The `seed_dev` script creates: - All 17 mock users from the frontend with real bcrypt-hashed passwords (password: `password`) - A representative set of properties, meetings, vendors, and projects - Matches the IDs used in the frontend (`c1`, `e1`, `own_001`, etc.) for a smooth integration transition --- ## 9. Running Locally ```bash # Start the API server uvicorn app.main:app --reload --port 8000 # OpenAPI docs (auto-generated) http://localhost:8000/docs # Swagger UI http://localhost:8000/redoc # ReDoc # Health check GET http://localhost:8000/health → { "status": "ok", "version": "1.0.0", "environment": "development" } ``` --- ## 10. Module Build Order (Recommended) Build modules in this sequence to avoid dependency blockers: ``` B2 (Schema) → B1 (Auth) → B3 (Properties) → B4 (Users) → B5 (Meetings) → B6 (Vendors/Compliance) → B7 (Financials) → B8 (Chatbot) → B9 (Notifications) ``` **Why schema first:** Every other module's router depends on the ORM models being defined. Define models and run `alembic revision --autogenerate` before writing a single router. --- *Next: Read `02_database_schema.md` before writing any ORM models.*