# LLD — Architecture (B0) **Scope:** FastAPI backend — internal structure, request lifecycle, auth flows, module dependencies **Companion doc:** `docs/backend/00_project_overview.md` --- ## Diagram 1 — FastAPI Application Layer Structure How the files inside `app/` relate to each other. Arrows show import/dependency direction. ```mermaid graph TD subgraph Entry["Entry Point"] MAIN["main.py\nFastAPI() instance\nMiddleware registration\nRouter registration"] end subgraph Config["Configuration"] CFG["config.py\nSettings (pydantic-settings)\nReads .env"] DB_MOD["database.py\nasync engine\nAsyncSession factory\nget_db()"] end subgraph Deps["Shared Dependencies"] DEPMOD["dependencies.py\nget_db()\nget_current_user()\nrequire_role(*roles)"] end subgraph Routers["Routers — /api/v1/"] R_AUTH["auth.py\n/auth/login\n/auth/refresh\n/auth/logout"] R_USERS["users.py\n/users/me\n/users/{id}"] R_PROP["properties.py\n/properties\n/properties/{id}"] R_MEET["meetings.py\n/meetings\n/meetings/{id}"] R_VEND["vendors.py\n/vendors\n/vendors/{id}"] R_PROJ["projects.py\n/projects\n/projects/{id}/tasks"] R_INV["invoices.py\n/invoices\n/invoices/{id}/approve"] R_DOC["documents.py\n/documents\n/documents/{id}"] R_CHAT["chatbot.py\n/chatbot/message"] end subgraph Services["Service Layer (Business Logic)"] S_AUTH["auth_service.py\nverify_password()\ncreate_tokens()\nrotate_refresh_token()"] S_PROP["property_service.py\nget_properties()\nassign_agent()\nmark_contacted()"] S_MEET["meeting_service.py\nschedule()\nchange_status()\nrequest_change()"] S_VEND["vendor_service.py\ncheck_compliance()\nupdate_coi()"] S_NOTIF["notification_service.py\nsend_email()\ncreate_notification()"] S_CHAT["chatbot_service.py\nbuild_context(role)\nproxy_to_groq()"] end subgraph Models["ORM Models (SQLAlchemy)"] M_ALL["user.py · property.py\nmeeting.py · vendor.py\nproject.py · invoice.py\ndocument.py · audit_log.py"] end subgraph Schemas["Pydantic Schemas (Request/Response)"] SCH_ALL["user.py · property.py\nmeeting.py · vendor.py\nauth.py · invoice.py"] end subgraph Utils["Utilities"] U_SEC["security.py\njwt_encode / jwt_decode\nbcrypt helpers"] U_PERM["permissions.py\nrole_can_access()"] U_MASK["mask.py\nmask_ssn() · mask_bank()"] end MAIN --> CFG MAIN --> DB_MOD MAIN --> DEPMOD MAIN --> R_AUTH & R_USERS & R_PROP & R_MEET & R_VEND & R_PROJ & R_INV & R_DOC & R_CHAT R_AUTH --> DEPMOD --> S_AUTH R_PROP --> DEPMOD --> S_PROP R_MEET --> DEPMOD --> S_MEET R_VEND --> DEPMOD --> S_VEND R_CHAT --> DEPMOD --> S_CHAT S_NOTIF --> S_PROP & S_MEET & S_VEND S_AUTH & S_PROP & S_MEET & S_VEND & S_PROJ --> M_ALL R_AUTH & R_PROP & R_MEET --> SCH_ALL S_AUTH --> U_SEC DEPMOD --> U_PERM R_USERS --> U_MASK ``` --- ## Diagram 2 — HTTP Request Lifecycle Every request follows this exact path through the backend. Each step is a named file/function. ```mermaid sequenceDiagram participant C as Client (React) participant CORS as CORS Middleware participant RL as Rate Limiter participant LOG as Request Logger participant RT as Router (e.g. properties.py) participant DEP as Dependencies participant SVC as Service Layer participant ORM as SQLAlchemy ORM participant DB as PostgreSQL C->>CORS: HTTP Request Note over CORS: Check Origin header
against ALLOWED_ORIGINS CORS-->>C: 403 if origin not allowed CORS->>RL: Pass through Note over RL: Check rate limit
by IP / user ID RL-->>C: 429 if exceeded RL->>LOG: Pass through Note over LOG: Log: method, path,
request_id, timestamp LOG->>RT: Route matched Note over RT,DEP: FastAPI resolves Depends() RT->>DEP: get_db() → yields AsyncSession RT->>DEP: get_current_user(token) Note over DEP: Decode JWT
Validate expiry
Load user from DB DEP-->>RT: 401 if invalid token RT->>DEP: require_role("ADMIN","OWNER") DEP-->>RT: 403 if role mismatch DEP-->>RT: current_user injected RT->>SVC: call service function(db, current_user, params) Note over SVC: Business logic
Permission refinement
Data transformation SVC->>ORM: db.execute(select(Property)...) ORM->>DB: SQL query DB-->>ORM: Rows ORM-->>SVC: Model instances SVC-->>RT: Result dict / model Note over RT: Pydantic response schema
serializes result RT-->>C: 200 { data: {...}, meta: {...} } Note over LOG: Log: status_code,
duration_ms, response_size ``` --- ## Diagram 3 — Authentication Flow ### 3a — Login ```mermaid sequenceDiagram actor U as User participant FE as React AuthContext participant API as POST /auth/login participant SVC as auth_service.py participant DB as users table participant SEC as security.py U->>FE: login(identifier, password, type) FE->>API: { identifier, password, type } API->>SVC: authenticate(identifier, password, type) alt type == "customer" SVC->>DB: SELECT WHERE username = identifier else type == "employee" (FIELD_AGENT / ADMIN) SVC->>DB: SELECT WHERE emp_id = identifier else type == "employee" (OWNER / CONTRACTOR / VENDOR / SUBCONTRACTOR) SVC->>DB: SELECT WHERE username = identifier end DB-->>SVC: User row (or None) alt User not found SVC-->>API: None API-->>FE: 401 { detail: "Invalid credentials" } FE-->>U: Toast "Invalid credentials" else User found SVC->>SEC: verify_password(plain, hash) alt Password wrong SEC-->>SVC: False SVC-->>API: None API-->>FE: 401 { detail: "Invalid credentials" } else Password correct SEC-->>SVC: True SVC->>SEC: create_access_token(user.id, user.role) SVC->>SEC: create_refresh_token(user.id) SVC->>DB: UPDATE users SET refresh_token_hash = ?, last_login_at = NOW() SVC-->>API: { access_token, refresh_token, user } API-->>FE: Set httpOnly cookie (access + refresh)\n200 { user: { id, name, role, ... } } FE-->>U: Redirect to role portal end end ``` ### 3b — Authenticated Request ```mermaid sequenceDiagram participant FE as React participant API as Any Protected Endpoint participant DEP as get_current_user() participant SEC as security.py participant DB as users table FE->>API: GET /properties\nCookie: access_token= API->>DEP: Depends(get_current_user) DEP->>DEP: Extract token from cookie / Authorization header alt No token present DEP-->>API: HTTPException 401 API-->>FE: 401 UNAUTHORIZED end DEP->>SEC: jwt_decode(token, SECRET_KEY) alt Token expired SEC-->>DEP: JWTError (ExpiredSignatureError) DEP-->>API: HTTPException 401 "Token expired" API-->>FE: 401 → FE triggers refresh flow else Token invalid (tampered) SEC-->>DEP: JWTError DEP-->>API: HTTPException 401 "Invalid token" else Token valid SEC-->>DEP: { sub: user_id, role: "FIELD_AGENT", exp: ... } DEP->>DB: SELECT WHERE id = user_id AND deleted_at IS NULL DB-->>DEP: User row DEP-->>API: current_user injected API->>API: require_role check API-->>FE: 200 with data end ``` ### 3c — Token Refresh ```mermaid sequenceDiagram participant FE as React (axios interceptor) participant API as POST /auth/refresh participant SVC as auth_service.py participant DB as users table participant SEC as security.py Note over FE: Receives 401 "Token expired" FE->>API: POST /auth/refresh\nCookie: refresh_token= API->>SVC: refresh_access_token(refresh_token) SVC->>SEC: jwt_decode(refresh_token) alt Refresh token invalid/expired SEC-->>SVC: JWTError SVC-->>API: Raise 401 API-->>FE: 401 → FE calls logout(), redirect to /login else Valid SEC-->>SVC: { sub: user_id } SVC->>DB: SELECT refresh_token_hash WHERE id = user_id SVC->>SEC: verify(incoming_token, stored_hash) alt Hash mismatch (token reuse attack) SEC-->>SVC: False SVC->>DB: UPDATE users SET refresh_token_hash = NULL (revoke all) SVC-->>API: 401 "Refresh token reuse detected" API-->>FE: 401 → Force logout else Hash matches SVC->>SEC: create_access_token(user_id, role) SVC->>SEC: create_refresh_token(user_id) SVC->>DB: UPDATE users SET refresh_token_hash = new_hash SVC-->>API: { new_access_token, new_refresh_token } API-->>FE: Set new cookies\n200 OK FE->>FE: Retry original request with new token end end ``` --- ## Diagram 4 — RBAC Middleware Decision Tree ```mermaid flowchart TD REQ["Incoming Request"] --> HAS_TOKEN{Token present\nin cookie\nor header?} HAS_TOKEN -->|No| R401_A["401 UNAUTHORIZED\nNo authentication provided"] HAS_TOKEN -->|Yes| DECODE{Decode JWT\nvalid signature?} DECODE -->|No / tampered| R401_B["401 UNAUTHORIZED\nInvalid token"] DECODE -->|Yes| EXPIRED{Token\nexpired?} EXPIRED -->|Yes| R401_C["401 UNAUTHORIZED\nToken expired\n→ client should refresh"] EXPIRED -->|No| LOAD_USER{User exists\nin DB and\nis_active?} LOAD_USER -->|No| R401_D["401 UNAUTHORIZED\nUser not found or deactivated"] LOAD_USER -->|Yes| HAS_ROLE_CHECK{Route has\nrequire_role()?} HAS_ROLE_CHECK -->|No - public route| PASS["✅ Proceed to handler\ncurrent_user = None"] HAS_ROLE_CHECK -->|Yes| ROLE_MATCH{user.role\nin allowed_roles?} ROLE_MATCH -->|No| R403["403 FORBIDDEN\nInsufficient permissions\nfor this resource"] ROLE_MATCH -->|Yes| HANDLER["✅ Proceed to handler\ncurrent_user = User object"] HANDLER --> SERVICE["Service layer\nfurther scoping\n(e.g. agent only sees own meetings)"] ``` --- ## Diagram 5 — Module Build Dependency Graph The recommended order to build backend modules. Each module depends on the ones above it. ```mermaid graph TD B0["B0\nProject Overview\n& Conventions"] B2["B2\nDatabase Schema\n(All ORM models)"] B1["B1\nAuthentication\n(JWT, RBAC, all 6 roles)"] B3["B3\nProperties Module\n(CRUD, geospatial, leads)"] B4["B4\nUsers & People\n(profiles, masking)"] B5["B5\nMeetings & Scheduling\n(status machine, change requests)"] B6["B6\nVendors & Compliance\n(COI, W9, expiry alerts)"] B7["B7\nFinancials\n(invoices, AR/AP, payouts)"] B8["B8\nChatbot & AI\n(RBAC context, Groq proxy)"] B9["B9\nNotifications\n(email, in-app, alerts)"] B0 --> B2 B2 --> B1 B1 --> B3 B1 --> B4 B3 --> B5 B4 --> B5 B5 --> B6 B3 --> B7 B6 --> B7 B3 & B4 & B5 & B6 & B7 --> B8 B5 & B6 & B7 --> B9 style B0 fill:#1e3a5f,color:#fff style B2 fill:#1e3a5f,color:#fff style B1 fill:#1e5f3a,color:#fff style B3 fill:#3a1e5f,color:#fff style B4 fill:#3a1e5f,color:#fff style B5 fill:#5f3a1e,color:#fff style B6 fill:#5f3a1e,color:#fff style B7 fill:#5f1e3a,color:#fff style B8 fill:#1e5f5f,color:#fff style B9 fill:#5f5f1e,color:#fff ``` --- ## Diagram 6 — Chatbot RBAC Context Builder How the chatbot service decides what data to inject into the LLM prompt based on the requesting user's role. ```mermaid flowchart TD REQ["POST /chatbot/message\n{ message, role }"] --> AUTH["Validate JWT\nExtract role"] AUTH --> ROLE{user.role?} ROLE -->|FIELD_AGENT| FA_CTX["Build Agent Context\n• Today's meetings\n• Assigned properties\n• Personal XP / streak\n• Top 5 hot leads in territory"] ROLE -->|ADMIN| ADM_CTX["Build Admin Context\n• Unassigned leads count\n• Today's schedule (all agents)\n• Pending signatures\n• Revenue MTD"] ROLE -->|OWNER| OWN_CTX["Build Owner Context\n• Project health scores\n• Overdue invoices\n• Compliance alerts\n• Revenue vs budget"] ROLE -->|CONTRACTOR| CON_CTX["Build Contractor Context\n• Assigned projects\n• Pending tasks\n• Open change orders"] ROLE -->|VENDOR| VEN_CTX["Build Vendor Context\n• Open orders\n• Compliance doc expiry\n• Payment status"] ROLE -->|CUSTOMER| CUS_CTX["Build Customer Context\n• Own property details\n• Upcoming meetings\n• Service history"] FA_CTX & ADM_CTX & OWN_CTX & CON_CTX & VEN_CTX & CUS_CTX --> PROMPT["Assemble system prompt\nRole context + user message"] PROMPT --> GROQ["POST Groq API\nqwen-32b\nstream=true"] GROQ --> AUDIT["Write audit_log\nactor_id, action=chatbot.query\nredacted message"] AUDIT --> STREAM["Stream response\nback to client"] ``` --- ## Diagram 7 — Environment Configuration Flow How environment variables flow from `.env` into every part of the app. ```mermaid graph LR ENV_FILE[".env file\n(never committed)"] ENV_EXAMPLE[".env.example\n(committed template)"] ENV_FILE -->|"pydantic-settings\nBaseSettings"| SETTINGS["config.py\nSettings singleton\nsettings.database_url\nsettings.secret_key\nsettings.groq_api_key\n..."] SETTINGS -->|"settings.database_url"| DB_MOD["database.py\ncreate_async_engine(url)"] SETTINGS -->|"settings.secret_key"| SEC["security.py\njwt_encode/decode"] SETTINGS -->|"settings.groq_api_key"| CHAT_SVC["chatbot_service.py\nhttpx.post(headers={api_key})"] SETTINGS -->|"settings.backend_cors_origins"| CORS_MW["main.py\nCORSMiddleware(allow_origins=...)"] SETTINGS -->|"settings.aws_*"| S3_SVC["document upload\nboto3.client(s3)"] SETTINGS -->|"settings.sendgrid_api_key"| NOTIF_SVC["notification_service.py\nSendGrid client"] ENV_EXAMPLE -.->|"documents expected vars\n(no real values)"| ENV_FILE ```