8.3 KiB
AI Weekly Synth -- Architecture Document
1. System Overview
AI Weekly Synth is a self-hosted Rust/Axum backend with a SolidJS frontend, backed by PostgreSQL, deployed as a Docker Compose stack. It generates AI-powered weekly news syntheses organized by user-configured themes and categories.
See requirements.md for product vision and features. See technical_specs.md for the full technology stack. See deployment.md for the Docker topology and operational details.
2. Layer Architecture
The backend follows a three-layer architecture with shared model types:
handlers/ (HTTP layer)
│
services/ (Business logic)
│
db/ (Data access)
│
models/ (Shared types -- used by all layers)
Handlers extract and validate request data, delegate to services or db, and format responses. Services contain all business logic. The db layer executes pure SQL via sqlx with typed result mapping and no business logic. Models define domain structs, request/response DTOs, and validation logic.
See dev_guidelines.md Section 2 for complete project structure.
3. Key Components
3.1 LLM Provider Abstraction
The LlmProvider trait defines a unified interface for all LLM backends, with implementations for Gemini, OpenAI, Anthropic, and a mock provider for testing. A factory creates provider instances by name from the admin-curated provider list.
See technical_specs.md Section 6 for provider interface details and supported models.
3.2 Synthesis Pipeline
The pipeline is orchestrated in services/synthesis.rs and runs as a background tokio task with a 15-minute timeout. Phase 1 processes the user's personalized sources using a rolling windowed extraction with batched parallel scraping and LLM classification. Phase 2 fills remaining category gaps via Brave Search or LLM web search. The finalization step assembles sections, persists the synthesis, and records article history. Progress is reported via tokio::sync::watch channels consumed by SSE endpoints.
See technical_specs.md Section 5 for the full algorithm.
3.3 Job Store
JobStore (services/job_store.rs) is an in-memory concurrent store for active generation jobs:
- Backed by
DashMap<Uuid, JobEntry>for lock-free access DashSet<Uuid>for per-user deduplication (one active job per user)- Each job holds a
watch::Sender<ProgressEvent>for real-time SSE streaming AtomicBoolfor cooperative cancellation- 1-hour TTL with automatic cleanup
3.4 Scheduler
services/scheduler.rs runs as a background task checking every minute for due theme_schedules. When a schedule fires it runs the generation pipeline directly, emails results to configured recipients (up to 3), and marks the schedule as run to prevent double-execution on the same day.
See deployment.md for operational details.
3.5 Scraper
Two scraping services:
scraper.rs: Article page scraper with SSRF prevention, HTML parsing, title/date/body extraction, soft-404 detection, 15s timeout, 5MB body limit.source_scraper.rs: Source index page scraper that extracts article links from user-configured source URLs (HTML<a>parsing with filters).
3.6 Rate Limiters
- Auth rate limiter: 10 requests/60s per key (email or IP) for magic link endpoints.
- Provider rate limiter: Per-LLM-provider sliding window, admin-configured, hot-reloaded from DB.
- User rate limiters: Per-user generation rate limits cached in
DashMap, recreated on settings change.
4. Data Model
Tables and Relationships
users
├── sessions (user_id FK, CASCADE)
├── magic_tokens (email reference, no FK)
├── settings (user_id PK/FK, CASCADE)
├── themes (user_id FK, CASCADE)
│ ├── sources (theme_id FK, CASCADE)
│ ├── syntheses (theme_id FK, SET NULL)
│ └── theme_schedules (theme_id FK, CASCADE, UNIQUE)
├── user_api_keys (user_id FK, CASCADE; UNIQUE per provider)
├── article_history (user_id FK, CASCADE)
├── llm_call_log (user_id FK, CASCADE)
└── audit_log (admin_user_id FK, SET NULL)
admin_providers
└── admin_rate_limits (provider_name FK, CASCADE)
See technical_specs.md Section 3 for complete column definitions.
5. API Overview
See technical_specs.md Section 4 for complete API endpoint specifications.
6. Security Architecture
Authentication & Session Management
- Passwordless: Magic link tokens sent via email (Resend API), single-use, time-limited
- Captcha: Cloudflare Turnstile on registration and login
- Sessions: SHA-256 hashed tokens stored in DB, 30-day expiry,
HttpOnly+SameSite=Laxcookies, optionallySecure - Anti-enumeration: Same response for existent/non-existent emails, timing attack mitigation
- Authorization:
AuthUserandAdminUserAxum extractors enforce auth levels per handler
CSRF Protection
All mutating API endpoints require the X-Requested-With header (checked by csrf::csrf_check middleware layer). Non-mutating GET/HEAD/OPTIONS requests are exempt.
Encryption at Rest
User LLM API keys are encrypted with AES-256-GCM before storage:
- 32-byte master key from
MASTER_ENCRYPTION_KEYenv var (64 hex chars) - Random 12-byte nonce per encryption (stored alongside ciphertext)
- Key bytes are zeroized on drop (
zeroizecrate) - Only a key prefix (first 8 chars + "...") is ever returned via the API
SSRF Prevention
Both scraper.rs and source_scraper.rs validate URLs before fetching:
- DNS resolution check against private/loopback IP ranges
- Redirect chain validation (no redirects to private IPs)
- Only HTTP/HTTPS schemes allowed
Security Headers
Applied as global middleware layers:
Content-Security-Policy(self + Cloudflare Turnstile)X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originX-XSS-Protection: 1; mode=blockStrict-Transport-Security(HTTPS only)
Error Sanitization
The sanitize_error_message function strips API keys and internal details from error messages before they reach SSE clients. Internal errors log full details server-side but return generic messages to users.
CORS
Configured to allow only the APP_URL origin, with credentials (cookies), limited to GET/POST/PUT/DELETE methods.
7. Concurrency Model
Async Runtime
Tokio with full features. The Axum server runs as a multi-threaded async runtime.
Background Tasks
Three tasks are spawned at startup: hourly session cleanup, periodic job store TTL cleanup, and the minute-by-minute theme schedule checker. See deployment.md Section 2.
Generation Pipeline Concurrency
tokio::task::JoinSet: Used for parallel scraping (bounded concurrency of 5 for source extraction) and parallel LLM classification calls within each batchtokio::sync::watch: Fan-out progress notifications to SSE clients; late subscribers immediately receive the latest stateAtomicBool: Cooperative cancellation flag checked between pipeline stages; avoids mutex overheadDashMap/DashSet: Lock-free concurrent access for the job store (job entries), generating-users set, per-user rate limiter cache, and provider rate limiter state
Task Lifecycle
POST /generate
└── handler creates job in JobStore
└── spawns outer task (panic monitor)
└── spawns inner task (15-min timeout)
└── run_generation_inner()
├── Phase 1 (JoinSet scrape, JoinSet classify)
├── Phase 2 (JoinSet scrape, JoinSet classify)
└── Save to DB
└── on complete/error: send final ProgressEvent
└── delayed cleanup (5 min) then remove from JobStore
Graceful Shutdown
The server supports graceful shutdown via signal handling, allowing in-flight requests to complete.
8. Quality Gates
- Release candidates must include deterministic CI coverage for critical autonomous flows, especially scheduler execution and SSE progress behavior.
- External-provider tests (for example live LLM E2E checks) are supplemental and non-blocking; they do not replace deterministic CI coverage.