7694788387
- Complete ProCanvas redesign with retro sports game aesthetic - Card-game style photo frame with tilt, shimmer, corner star accents - Daily Missions card with weekly challenge + individual quest progress bars - Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting) - Each log action opens themed modal with relevant input fields - Challenges modal with 6 active challenges and progress tracking - Achievements modal with all badges, unlock status, descriptions - Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals - Rewards & Checkpoints with named stages (Daily Grind to Legend Run) - Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings) - Hit The Map button with smooth pulsing glow animation - Grid pattern overlay in Leaderboard card - Light mode support via dark: Tailwind variants throughout - Top 3 badges displayed on profile card - Fixed dropdown option visibility in dark mode
434 lines
13 KiB
Markdown
434 lines
13 KiB
Markdown
# 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 <token>`
|
|
|
|
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.*
|