6.9 KiB
08 — Technical Architecture (Phase 7)
How the redesign is built on the existing Clean Architecture — extending seams, not
rewriting. Everything here preserves the dependency rule Api → Infrastructure → Application → Domain and the security posture from 01.
Update — multi-provider platform. This architecture now assumes the multi-provider design: the Gmail-centric
Sendermodel generalises to account-scoped, provider-normalised entities behindIEmailProvider; the app becomes small-team multi-user (Admin/Member) with OAuth-as-login, settings, and feature flags; and AI gating is feature-flag-driven (ai.enabledsystem flag → user pref). See that folder for the provider abstraction, DB schema, security model, and admin/settings design; this doc remains the search/AI/ caching/indexing reference they build on.
Overall
React SPA (v2 shell)
│ REST (+ SSE for streaming AI/search)
Api ──────────────────────────────────────────────
Application: IInboxAi · ISearchService · SearchIntentParser · DTOs
Infrastructure:
├ Search: SearchService (structured + lexical rank + hybrid)
├ AI: IAiProvider / IEmbeddingProvider / model router / VRAM guard
├ Jobs: EmbeddingBackfill · AiEnrichment · IndexMaintenance (IHostedService + Channel queue)
├ Gmail/Sync/Cleanup/Analytics (existing)
└ Persistence: EF Core + Npgsql (+ pgvector)
Domain: entities/enums (+ Embedding, Entities, ThreadSummary, SavedSearch)
External (optional): Ollama (local, own container) · cloud AI (opt-in only)
Search architecture
The 4-layer engine from 05:
- Structured (SQL WHERE) + Lexical (
websearch_to_tsquery+ts_rank_cd, weighted multi-fieldtsvector,pg_trgmfuzzy fallback) — always on. - Semantic (pgvector, HNSW) + AI-assisted (intent parse, RAG) — optional.
- Hybrid fusion (RRF) merges lexical+semantic; boosts recency/sender/unread.
- Keyset pagination replaces offset; score-ordered top-N for ranked queries.
SearchIntentParser(evolvesGmailQueryParser): operators → rules-NL → optional LLM.
AI architecture
- Provider interfaces +
IInboxAifacade + model router (task→model via config) + prompt templates (versioned files) + VRAM guard (serialises heavy vision jobs behind unloading the 7B; embeddings stay hot). See 06. - Streaming via SSE for summaries/replies/RAG answers (perceived speed).
- Bounded: per-call timeout,
CancellationToken, Polly fallback to Null path.
Plugin / modularity architecture
- Analyzer pipeline: email enrichment is a set of
IEmailAnalyzerplugins (classifier, entity-extractor, summariser, phishing, dedup). Each declares required capabilities (Chat/Embeddings/none) and is skipped gracefully if unavailable. New AI features = new analyzers; no core changes. - Search providers implement a common
ISearchLayerso semantic/AI layers plug in. - AI providers already pluggable (
IAiProvider); adding a provider = one class + config. - This is the "modular AI" requirement realised structurally.
Caching
- Embeddings & summaries: persisted in Postgres (compute once, invalidate on new msg).
- Search suggestions / recent: cached per-user (memory + DB); debounced live search.
- Query results: short-TTL cache for identical repeated queries; ETag on read endpoints.
- Model warmth: Ollama
keep_alivekeeps the 7B hot between calls.
Indexing
| Index | Column | Purpose |
|---|---|---|
| GIN | SearchVector (weighted A/B/C/D) |
Full-text |
GIN pg_trgm |
sender addr/name, subject | Fuzzy / substring (fixes non-sargable .Contains) |
| HNSW | Email.Embedding (pgvector) |
Semantic k-NN |
| btree | (UserId, SentAtUtc), (UserId, ThreadId) |
Keyset pagination, threading |
- Built incrementally on sync; EmbeddingBackfillWorker batch-fills history (VRAM-aware, low priority).
Background jobs
- Keep the existing
IHostedServiceworker pattern; add aChannel<T>in-process queue with a bounded concurrency worker for AI enrichment (summaries/entities/embeddings on new mail). Idempotent, resumable, backpressured. (Upgrade path: durable queue if multi-node.) - Jobs:
GmailSyncWorker(exists),DigestWorker(exists),EmbeddingBackfillWorker,AiEnrichmentWorker,IndexMaintenanceWorker.
Database schema improvements
- Widen
Email.SearchVector(subject/sender/filename/labels, weighted). - Add:
Email.Embedding vector(768); tablesEmailEntities,ThreadSummary,SavedSearch,SearchHistory,AiJob(status/retry),SenderImportance(materialised). - Fix debt: resolve the
Email↔EmailLabelglobal-query-filter warning (optional nav or matching filters); make FTS language-aware (detect → per-language config) instead of hardcoded English. - Scale: consider per-
UserIdlist partitioning ofEmailsif single users exceed ~1M rows.
Scalability
- Single-user/self-host is the primary shape → vertical scaling + good indexes is enough.
- Connection pooling (Npgsql), keyset pagination, top-N-by-score, batched embeddings.
- Ollama is the throughput bottleneck → serialise/queue AI, cache aggressively, prefer the small/embedding models for high-volume paths.
- Multi-tenant future: read replicas, per-user partitioning, durable job queue.
Deployment
- Extend the Compose stack (from the CI/CD we built): add an optional
ollamaservice (profileai) and a pgvector-enabled Postgres image. AI-off deployments omit the profile entirely. Dev→staging auto-deploy already proven; prod is tag-gated.
Offline support
- Data is already local (Postgres on the user's machine) — the product is local-first by nature. SPA: service-worker cache for shell + last-viewed mail (read offline); queue mutations (label/cleanup) to replay on reconnect. AI is inherently offline (Ollama local).
Security model
- Retain: OAuth read-only scope, encrypted refresh tokens (Data Protection), IDOR global query filters, SSRF egress guard, non-root containers, confirmed+previewed destructive actions.
- New AI concerns:
- Prompt injection — email content is untrusted input to the LLM. Treat model output as advisory only; never let it trigger actions directly; sanitise/format; the LLM can suggest but a human/rule confirms (aligns with "AI never acts").
- Attachment/vision — sandbox parsing; size/type limits; never execute.
- Local-only by default — cloud provider is explicit opt-in with per-feature consent and egress logging; the SSRF guard already constrains outbound calls.
- Secrets stay in
deploy/.env/ Actions secrets (never in git) — as established.