# Architecture
## System Architecture
```mermaid
graph TB
subgraph Client
GM[Gmail UI]
CE[Chrome Extension]
GM --> CE
end
subgraph Backend["FastAPI Backend :8000"]
MW[Middleware Layer]
subgraph MW
CORS[CORS Filter]
RL[Rate Limiter]
AUTH[API Key Auth]
end
subgraph API["API Layer /v1"]
H[GET /health]
P[POST /predict]
PB[POST /predict/batch]
F[POST /feedback]
FS[GET /feedback/summary]
R[POST /retrain]
end
subgraph Engine["Detection Engine"]
W[1. Whitelist]
TC[2. Trusted Catalog]
RB[3. Rule-Based]
BC[4. Benign Context]
ML[5. ML Model]
end
subgraph MLSub["ML Subsystem"]
VEC[TfidfVectorizer]
ENS[EnsemblePredictor
XGBoost + DeBERTa-v3]
EXP[Explanation Engine]
end
subgraph Security["Security"]
PII[PII Redaction]
SHA[SHA-256 Integrity]
end
FSYS[Feedback Store]
end
subgraph Storage["Storage Layer"]
JSONL[feedback.jsonl]
MYSQL[MySQL 8.0]
MODEL[model artifacts]
end
CE -->|HTTP/HTTPS| CORS
CORS --> RL
RL --> AUTH
AUTH --> API
P -->|secured only| F
R -->|secured only| AUTH
P --> Engine
PB --> Engine
Engine --> MLSub
MLSub --> EXP
Engine --> PII
SHA --> MODEL
F --> FSYS
FSYS --> JSONL
FSYS --> MYSQL
R --> FSYS
R --> MODEL
H --> FSYS
```
## Request Flow
```mermaid
sequenceDiagram
participant Gmail as Gmail
participant Ext as Chrome Extension
participant API as FastAPI
participant Detector as Detection Engine
participant Store as Feedback Store
Gmail->>Ext: User opens email
Ext->>API: POST /v1/predict
API->>API: CORS check
API->>API: Rate limit check
API->>Detector: predict_email(sender, subject, body)
Detector->>Detector: PII redaction
Detector->>Detector: 1. Whitelist?
Detector->>Detector: 2. Trusted catalog?
Detector->>Detector: 3. Rule-based spam?
Detector->>Detector: 4. Benign context?
Detector->>Detector: 5. ML classification
Detector->>Detector: Build explanations
Detector-->>API: PredictionResult
API-->>Ext: label, confidence, explanations
Ext-->>Gmail: Display overlay banner
opt User submits feedback
Ext->>API: POST /v1/feedback (auth required)
API->>Store: append_feedback_entry()
Store-->>API: stored
API-->>Ext: feedback_id, verdict
end
```
## Prediction Flow (5-Layer Pipeline)
```mermaid
flowchart TD
START([Email Received]) --> REDACT[PII Redaction]
REDACT --> DOMAIN[Extract Sender Domain]
DOMAIN --> WL{Whitelist?}
WL -->|Yes| WL_RESULT[Label: whitelisted
Confidence: 1.0]
WL -->|No| TC{Trusted Catalog?}
TC -->|Yes| TC_RESULT[Label: Not Spam
Confidence: 0.97
Layer: trusted_service]
TC -->|No| RULES{Rule-Based Spam?}
RULES -->|Yes| RULES_RESULT[Label: Spam
Confidence: 0.86-0.99
Layer: rules]
RULES -->|No| BENIGN{Benign Context?}
BENIGN -->|Yes| BENIGN_RESULT[Label: Not Spam
Confidence: 0.76-0.82
Layer: benign_context/promo]
BENIGN -->|No| ML_LAYER[ML Classification]
ML_LAYER --> BUILD[Build Feature Matrix]
BUILD --> PREDICT[Ensemble.predict_proba]
PREDICT --> THRESHOLD{spam_prob >= threshold?}
THRESHOLD -->|Yes| SPAM[Label: Spam
Layer: ml]
THRESHOLD -->|No| HAM[Label: Not Spam
Layer: ml]
SPAM --> EXPLAIN
HAM --> EXPLAIN
EXPLAIN[Generate Explanations] --> RESPONSE([Return PredictionResult])
```
### Layer Details
| Layer | Decision Logic | Confidence | Explanation |
|---|---|---|---|
| **Whitelist** | Sender domain in user whitelist CSV | 1.0 | "Trusted sender matched your local whitelist" |
| **Trusted Catalog** | Sender domain in built-in service catalog | 0.97 | "Trusted service domain matched the curated built-in catalog" |
| **Rule-Based** | >=2 spam phrases or >=1 phrase + >=2 signals | 0.86–0.99 | Matched phrases and indicator signals |
| **Benign Context** | Conversational wording, no links/urgency | 0.82 | "Benign-context detection found conversational wording without phishing indicators" |
| **Benign Promo** | Promotional wording, no links, low caps | 0.76 | "Benign promotional detection found retail language without phishing indicators" |
| **ML Model** | Ensemble (XGBoost + DeBERTa-v3) spam probability >= threshold | 0.00–0.99 | Top contributing features from model coefficients |
## Feature Matrix
The ML model operates on a combined feature matrix built from:
```mermaid
flowchart LR
RAW[Raw Email Text] --> PREPROC[NLP Preprocessing]
PREPROC --> WORD[Word TF-IDF]
PREPROC --> CHAR[Char TF-IDF]
RAW --> META[Meta Features]
WORD --> CONCAT[sp.hstack]
CHAR --> CONCAT
META --> CONCAT
CONCAT --> MATRIX[Combined CSR Matrix]
MATRIX --> MODEL[Ensemble
XGBoost + DeBERTa]
MODEL --> PROB[spam_prob, ham_prob]
```
### Meta Features (32-dim)
| # | Feature | Description |
|---|---|---|
| 1 | `url_count` | Number of URLs detected |
| 2 | `caps_ratio` | Ratio of uppercase letters |
| 3 | `exclamation_count` | Count of `!` characters |
| 4 | `question_count` | Count of `?` characters |
| 5 | `money_count` | Money amounts (all currencies) detected |
| 6 | `phone_count` | Phone numbers detected |
| 7 | `word_count` | Total word count |
| 8 | `avg_word_length` | Average word length |
| 9 | `digit_ratio` | Ratio of digit characters |
| 10 | `spam_phrase_hits` | Known phishing phrase matches |
| 11 | `urgency_hits` | Urgency keyword matches |
| 12 | `account_hits` | Account/security keyword matches |
| 13 | `call_to_action_hits` | CTA keyword matches |
| 14 | `symbol_ratio` | Ratio of symbol characters |
| 15 | `percent_hits` | Count of `%` characters |
| 16 | `mixed_token_hits` | Mixed letter-number tokens |
| 17–24 | (URL analysis, HTML, obfuscation) | Advanced detection features |
| 25–32 | (Credential, keyword, attachment) | Phishing-specific features |
> Full 32-feature table in [MODEL_ARCHITECTURE.md](../MODEL_ARCHITECTURE.md#32-meta-features).
## Retraining Flow
```mermaid
sequenceDiagram
participant Ext as Extension
participant API as /v1/retrain
participant Lock as RETRAIN_LOCK
participant SP as subprocess.run
participant Train as train_model.py
participant Load as load_resources()
Ext->>API: POST /v1/retrain (auth required)
API->>Lock: acquire(blocking=False)
alt Lock acquired
API->>SP: subprocess.run(train_model.py)
SP->>Train: Execute training pipeline
alt Success (returncode=0)
Train-->>SP: model artifacts saved
SP-->>API: CompletedProcess
API->>Load: Reload model from disk
Load-->>API: New model loaded
API-->>Ext: 200 - model_version, metrics
else Timeout
SP-->>API: TimeoutExpired
API-->>Ext: 500 - "Retraining timed out"
else Failure
SP-->>API: returncode != 0
API-->>Ext: 500 - stderr output
end
API->>Lock: release()
else Lock not acquired
API-->>Ext: 409 - "Retraining already in progress"
end
```
## Storage Flow
```mermaid
flowchart TD
FEEDBACK[POST /v1/feedback] --> RESOLVE[resolve_feedback_store]
RESOLVE --> MODE{SPAM_FEEDBACK_BACKEND}
MODE -->|file| FILE[append_feedback_file
Write JSONL line]
MODE -->|mysql| MYSQL[append_feedback_mysql
CREATE TABLE IF NOT EXISTS
INSERT with parameterized query]
MODE -->|auto| CHECK{DB_HOST + DB_USER + DB_NAME?}
CHECK -->|configured| MYSQL
CHECK -->|not configured| FILE
RETRAIN[POST /v1/retrain] --> TRAIN[train_model.py]
TRAIN --> LOAD_FB[load_feedback_entries]
LOAD_FB --> FB_MODE{feedback backend}
FB_MODE -->|file| FB_FILE[Read JSONL lines]
FB_MODE -->|mysql| FB_MYSQL[SELECT * FROM table]
FB_FILE --> COLLAPSE[Collapse duplicates
Map labels to 0/1]
FB_MYSQL --> COLLAPSE
COLLAPSE --> TRAIN_ML[Train with feedback samples]
```
## Security Flow
```mermaid
flowchart TD
REQ[Incoming Request] --> CORS{CORS Origin?}
CORS -->|Invalid| REJECT_CORS[No CORS headers]
CORS -->|Valid| RATE{Rate Limit?}
RATE -->|Exceeded| REJECT_429[429 Too Many Requests]
RATE -->|OK| AUTH{Auth Required?}
AUTH -->|No| PROCESS[Process Request]
AUTH -->|Yes| KEY{API Key Valid?}
KEY -->|No| REJECT_401[401 Unauthorized]
KEY -->|Yes| PROCESS
PROCESS --> PII[PII Redaction]
PII --> DETECT[Detection Pipeline]
DETECT --> RESPONSE[Response]
```
### Model Integrity on Startup
```mermaid
flowchart TD
START[App Startup] --> LOAD[load_model]
LOAD --> EXISTS{model & vectorizer exist?}
EXISTS -->|No| RETURN_NONE[Return None
Predict returns 500]
EXISTS -->|Yes| HASH{Hash file exists?}
HASH -->|No| LOAD_FILES[Load pickle files without check]
HASH -->|Yes| VERIFY[hmac.compare_digest]
VERIFY -->|Match| LOAD_FILES
VERIFY -->|Mismatch| ERROR[ModelIntegrityError
App crashes]
```
## Module Dependency Graph
```mermaid
graph TD
MAIN[app/main.py] --> CONFIG[app/config.py]
MAIN --> ROUTER[app/api/v1/router.py]
MAIN --> DOMAIN[app/core/domain.py]
MAIN --> REGISTRY[app/ml/registry.py]
MAIN --> PREDICT[app/api/v1/predict.py]
MAIN --> HEALTH[app/api/v1/health.py]
MAIN --> FEEDBACK[app/api/v1/feedback.py]
MAIN --> RETRAIN[app/api/v1/retrain.py]
ROUTER --> PREDICT
ROUTER --> HEALTH
ROUTER --> FEEDBACK
ROUTER --> RETRAIN
PREDICT --> DETECTOR[app/core/detector.py]
HEALTH --> FEEDBACK_STORE[app/storage/feedback.py]
DETECTOR --> ENSEMBLE[app/ml/ensemble.py]
DETECTOR --> DOMAIN
DETECTOR --> EXPLAIN[app/core/explain.py]
DETECTOR --> FEATURES[app/core/features.py]
DETECTOR --> RULES[app/core/rules.py]
DETECTOR --> TEXT[app/core/text.py]
DETECTOR --> PII_UTILS[app/utils/pii.py]
FEATURES --> CONSTANTS[app/core/constants.py]
RULES --> CONSTANTS
RULES --> FEATURES
TEXT --> CONSTANTS
EXPLAIN --> CONSTANTS
RETRAIN --> MAIN
RETRAIN --> FEEDBACK_STORE
RETRAIN --> AUTH[app/core/auth.py]
FEEDBACK --> AUTH
FEEDBACK --> FEEDBACK_STORE
REGISTRY --> MODEL_FILES[(model artifacts)]
DOMAIN --> CSV_FILES[(CSV data files)]
FEEDBACK_STORE --> JSONL[(feedback.jsonl)]
FEEDBACK_STORE --> MYSQL_DB[(MySQL)]
```
## Key Design Decisions
1. **Module-level model state**: The prediction, health, feedback, and retrain modules hold their state as module-level variables rather than dependency injection. This simplifies the codebase and avoids passing state through every function, at the cost of test isolation complexity (tests patch module state).
2. **CSV for configuration data**: Whitelist and trusted domain catalogs use CSV files rather than a database. This keeps the system self-contained and deployable with zero infrastructure dependencies. CSV files are read once at startup.
3. **JSONL for feedback**: Feedback entries are stored as newline-delimited JSON, making the storage human-readable, version-controllable, and trivially portable. MySQL is offered as an optional upgrade path for multi-instance deployments.
4. **SHA-256 sidecar files**: Model integrity uses hash sidecar files (`model.pkl.sha256`) rather than embedded checksums, allowing hash verification to be retrofitted to existing models and updated independently.
5. **Concurrency lock on retraining**: A `threading.Lock` prevents overlapping retraining jobs. The lock is non-blocking — concurrent requests receive 409 instead of queuing.
6. **PII at the boundary**: PII redaction occurs at the API entry point (`predict_email`) rather than in storage, ensuring redacted data never reaches the feedback store or model training pipeline.
instead of queuing.
6. **PII at the boundary**: PII redaction occurs at the API entry point (`predict_email`) rather than in storage, ensuring redacted data never reaches the feedback store or model training pipeline.