# Guida Multi-Region — adiuvAI API > Stato attuale: FastAPI containerizzata (docker-compose) su singolo VPS Hetzner in Europa. > Obiettivo: ridurre la latenza per utenti fuori dall'Europa. --- ## Fase 1 — Ottimizzare Cloudflare (già in uso) ### 1.1 Argo Smart Routing - **Dashboard → Traffic → Argo** — attivalo (~$5/mese + $0.10/GB) - Usa i backbone privati Cloudflare invece dell'internet pubblico - Riduce la latenza del 30-40% senza toccare nulla lato server - Singolo cambiamento con miglior rapporto costo/beneficio ### 1.2 SSL/TLS - **Dashboard → SSL/TLS → Overview** → mode **Full (Strict)** (non "Flexible", causa redirect loop) - Abilita **TLS 1.3** (meno round-trip nell'handshake) - Abilita **Early Hints** (103) in Speed → Optimization ### 1.3 Cache Rules Di default Cloudflare non cachea le risposte API (Content-Type `application/json`). Per gli endpoint pubblici (es. `/api/v1/health`): - **Dashboard → Caching → Cache Rules** → crea regola: - Match: `URI Path starts with /api/v1/health` - Action: Cache, Edge TTL 30s, Browser TTL 10s - Lato codice: aggiungere header `Cache-Control: public, s-maxage=30` e `CDN-Cache-Control: public, max-age=30` all'health endpoint - **NON** cacheate endpoint autenticati (il JWT rende ogni richiesta unica) ### 1.4 Response Compression - **Dashboard → Speed → Optimization → Content Optimization** - Abilita **Brotli** (più efficiente di gzip per payload JSON) - Le risposte JSON vengono compresse automaticamente al transit ### 1.5 WebSocket - **Dashboard → Network** → verifica che **WebSockets** sia ON (default nel piano Free) - Il `/chat/stream` WebSocket viene proxato ma non cacheato - Il keepalive di 30s che già avete mantiene la connessione viva attraverso Cloudflare ### 1.6 Tiered Cache (piano Pro+) - **Dashboard → Caching → Tiered Cache** → attiva **Smart Tiered Caching** - Cloudflare usa data center "upper-tier" come cache intermedia - Riduce le hit al tuo origin server ### 1.7 Timeout - **Dashboard → Network → WebSocket timeout**: aumenta se gli utenti hanno sessioni chat lunghe - **Proxy Read Timeout**: default 100s, sufficiente per le LLM call (il tool loop ha cap 5 iterazioni) --- ## Fase 2 — Secondo nodo in US East ### Architettura target ``` ┌─── Cloudflare (Geo Steering) ───┐ │ │ utenti EU/Africa utenti Americas │ │ ┌────────▼─────────┐ ┌──────────▼─────────┐ │ VPS EU (attuale) │ │ VPS US (nuovo) │ │ docker-compose │ │ docker-compose │ │ app + PG primary │ │ app + PG replica │ └────────┬──────────┘ └──────────┬──────────┘ │ │ └── PG streaming replication ────────┘ (async, read-only replica) ``` ### Opzione A: Secondo VPS Hetzner (Ashburn) + Cloudflare Load Balancing Estensione naturale del setup attuale — minimo cambiamento architetturale. #### Step 1 — Provisioning del VPS US 1. Crea un VPS Hetzner in **Ashburn (us-east)** (stesse specs del nodo EU) 2. Setup identico: Docker, Docker Compose, git 3. Configura un **tunnel WireGuard** tra EU e US per il traffico DB (mai esporre PG sulla rete pubblica) #### Step 2 — PostgreSQL Streaming Replication **Sul PRIMARY (EU):** 1. Creare un utente replication: ```sql CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD ''; ``` 2. Creare un replication slot: ```sql SELECT pg_create_physical_replication_slot('replica_us_east'); ``` 3. Configurare `pg_hba.conf` per permettere connessioni dal VPS US: ``` host replication replicator /32 scram-sha-256 ``` 4. Esporre la porta PG solo sull'IP WireGuard nel `docker-compose.yml`: ```yaml services: db: ports: - "10.0.0.1:5432:5432" # solo interfaccia WireGuard ``` **Sul REPLICA (US):** 1. Base backup dal primary: ```bash docker run --rm \ -v postgres_data:/var/lib/postgresql/data \ pgvector/pgvector:pg16 \ bash -c "pg_basebackup -h -U replicator \ -D /var/lib/postgresql/data -Fp -Xs -P -R" ``` Il flag `-R` crea automaticamente `standby.signal` e scrive `primary_conninfo` in `postgresql.auto.conf`. 2. Avviare PG in modalita replica (legge `standby.signal` e si connette al primary) 3. Verificare: - Sul primary: `SELECT * FROM pg_stat_replication;` (deve mostrare il replica) - Sul replica: `SELECT pg_is_in_recovery();` (deve restituire `t`) #### Step 3 — Modifiche al codice FastAPI Modifiche necessarie in `app/config/settings.py`: - Aggiungere `DATABASE_READ_URL: str = ""` — URL del replica locale per le letture - Aggiungere `REGION: str = "eu"` — identificativo regione per health check e observability Modifiche in `app/db.py`: - Creare un secondo engine `read_engine` che usa `DATABASE_READ_URL` (fallback a `DATABASE_URL` se vuoto) - Esporre un `get_read_session()` dependency da usare nelle query read-only Modifiche in `app/main.py`: - L'health endpoint deve restituire `region` nel payload - Aggiungere header `Cache-Control` / `CDN-Cache-Control` per il caching all'edge Nelle route, per le query di sola lettura pesanti (es. ricerca, listing): - Usare `db: AsyncSession = Depends(get_read_session)` invece di `get_session` - Le scritture (auth, billing, update) continuano a usare `get_session` (→ primary EU) #### Step 4 — Docker Compose per il nodo US Creare un `docker-compose.replica.yml` (override) che: - Sovrascrive le env dell'app con `DATABASE_READ_URL` verso il DB locale e `DATABASE_URL` verso il primary EU - Imposta `REGION=us-east` - Avvia PG in modalita replica (con `primary_conninfo` che punta al primary EU via WireGuard) Il `.env` sul nodo US: ```env DATABASE_URL=postgresql+asyncpg://postgres:@:5432/adiuvai DATABASE_READ_URL=postgresql+asyncpg://postgres:postgres@db:5432/adiuvai REGION=us-east PRIMARY_DB_HOST= REPLICATOR_PASSWORD= # ... resto delle variabili (JWT_SECRET, STRIPE, LLM keys, etc.) identiche al nodo EU ``` Avvio: `docker compose -f docker-compose.yml -f docker-compose.replica.yml up -d` #### Step 5 — Deploy CI multi-region Estendere il workflow `.gitea/workflows/deploy.yaml` con un secondo job `deploy-us`: - Identico a `deploy` ma con SSH verso il VPS US - Usa `secrets.SSH_HOST_US`, `secrets.SSH_USER_US`, `secrets.SSH_KEY_US` - Il comando di deploy usa `-f docker-compose.yml -f docker-compose.replica.yml` - **NON** esegue `alembic upgrade head` — le migrazioni girano solo sul primary (il replica riceve le DDL via replication) I due job `deploy` e `deploy-us` possono girare in parallelo (entrambi dipendono solo da `test`). #### Step 6 — Cloudflare Geo Steering 1. **Dashboard → Traffic → Load Balancing** (piano Pro, ~$5/mese per pool) 2. Creare due **Origin Pools**: - `eu-pool`: origin = IP del VPS EU, health check = `GET /api/v1/health` - `us-pool`: origin = IP del VPS US, health check = `GET /api/v1/health` 3. Creare un **Load Balancer** su `api.adiuvai.com`: - Steering policy: **Geo** - EU/Africa → `eu-pool` - Americas → `us-pool` - Fallback: `eu-pool` 4. Impostare health monitor: `GET /api/v1/health`, interval 60s, timeout 5s - Se un nodo va giù, tutto il traffico va al nodo sano (automatic failover) ### Opzione B: Fly.io (alternativa più semplice, meno controllo) Se preferisci evitare la gestione manuale di un secondo VPS: 1. Crea un `fly.toml` nella root del progetto API 2. `fly launch` — Fly rileva il Dockerfile e deploya 3. `fly regions add iad` — aggiunge US East (Ashburn) 4. Fly gestisce: routing anycast, health checks, TLS, auto-scaling 5. Il DB resta su Hetzner EU — Fly non risolve il problema del database, ma elimina tutta la gestione infrastrutturale dell'app layer 6. Costo: ~$5-15/mese per region (dipende dalle risorse) 7. Contro: meno controllo, vendor lock-in, il DB non ha replica locale ### Opzione C: Hetzner Cloud Load Balancer + geo DNS esterno - Hetzner offre load balancer nativi, ma sono single-region (non cross-region) - Non adatto per geo-routing, utile solo per HA nella stessa region --- ## Fase 3 — Terzo nodo in Asia (futuro) Stessa procedura della Fase 2: 1. VPS Hetzner Singapore (o AWS ap-southeast-1) 2. Secondo PG replica con slot `replica_asia` 3. Terzo pool in Cloudflare Load Balancing con geo steering per Asia-Pacific 4. Terzo job `deploy-asia` nel CI Da valutare solo quando il traffico dall'Asia lo giustifica. --- ## Sicurezza della rete tra i nodi | Metodo | Pro | Contro | |--------|-----|--------| | **WireGuard** | Semplice, veloce, <1ms overhead, kernel-level | Setup manuale per nodo | | **Hetzner vSwitch** | Zero config se entrambi su Hetzner | Solo stessa region | | **Tailscale** | WireGuard gestito, zero config rete | Dipendenza esterna | | **SSH tunnel** | Nessun software extra | Overhead maggiore, meno stabile | **Raccomandazione**: WireGuard (o Tailscale per semplicita) tra tutti i nodi. Mai esporre PostgreSQL 5432 sull'IP pubblico. --- ## Considerazioni specifiche per adiuvAI - **L'app e local-first**: la maggior parte delle operazioni (tasks, notes, projects) avviene in SQLite locale nell'Electron app. Il backend serve solo auth, chat streaming, cloud storage e billing. Questo significa che la latenza del backend impatta meno di quanto sembrerebbe. - **WebSocket `/chat/stream`**: il geo steering porta l'utente al nodo piu vicino, ma la risposta LLM dipende dalla latenza verso OpenAI/Anthropic (non verso il tuo server). Il beneficio principale e nel tempo di handshake e nel primo token. - **`_pending_states` in-memory per OAuth**: gia documentato come non scalabile su multi-worker. Con multi-region diventa critico — servira Redis condiviso o spostare lo state su DB. - **JWT_SECRET deve essere identico** su tutti i nodi — un token emesso dal nodo EU deve essere validato dal nodo US. - **Alembic migrations**: eseguire SOLO sul primary. Il replica riceve le DDL via streaming replication. --- ## Stima costi | Componente | Costo mensile | |------------|---------------| | Argo Smart Routing | ~$5 + $0.10/GB | | Cloudflare Load Balancing | ~$5/pool | | VPS Hetzner US (CX22) | ~$5-10 | | WireGuard | Gratis | | **Totale Fase 1** | **~$5** | | **Totale Fase 2** | **~$15-20** |