Skip to content

Architecture

Valerio edited this page Apr 21, 2026 · 3 revisions

Architecture

Harpyx is split along two orthogonal axes: layers (Clean Architecture) and processes (web-facing vs. background).

Layered design

┌────────────────────────────────────────────────────────────────┐
│   Presentation                                                 │
│   ├── Harpyx.WebApp    (Razor Pages + REST API + OpenAPI)      │
│   └── Harpyx.Worker    (background hosted services)            │
├────────────────────────────────────────────────────────────────┤
│   Application                                                  │
│       Harpyx.Application — services, DTOs, interfaces,         │
│                            telemetry sources, domain rules     │
├────────────────────────────────────────────────────────────────┤
│   Domain                                                       │
│       Harpyx.Domain — entities, enums, no framework deps       │
├────────────────────────────────────────────────────────────────┤
│   Infrastructure                                               │
│       Harpyx.Infrastructure — EF Core, MinIO, RabbitMQ, Redis, │
│                               OpenSearch, LLM clients, email,  │
│                               malware scanner, encryption      │
└────────────────────────────────────────────────────────────────┘

Dependency rules

  • Harpyx.Domain depends on nothing.
  • Harpyx.Application depends only on Harpyx.Domain and abstractions from Microsoft.Extensions.*.
  • Harpyx.Infrastructure depends on Harpyx.Domain and Harpyx.Application; it implements the interfaces declared in Application.Interfaces.
  • Harpyx.WebApp and Harpyx.Worker depend on Harpyx.Application (for services) and Harpyx.Infrastructure (for DI registration of concrete implementations). They do not reference Harpyx.Domain directly except transitively.
  • Harpyx.Shared holds cross-cutting utilities that are framework-agnostic.

This keeps the business logic independent of the backing infrastructure, so swapping (say) OpenSearch for a different vector store only requires a new implementation of IRagRetrievalService / OpenSearchChunkIndexService — no changes elsewhere.

Process topology

Two independent processes share the same data plane:

Process Concern
Harpyx.WebApp HTTP ingress, authentication, CSRF, rate limiting, uploads, chat, admin UI, REST API
Harpyx.Worker RabbitMQ consumer for ParseDocumentJob, scheduled cleanup for expired projects

Both processes go through the same configuration override chain (Configuration) and register the infrastructure services via AddHarpyxInfrastructure. What differs is the hosted-services set and the HTTP pipeline.

Key backing services

Dependency Used for
SQL Server EF Core persistence: tenants, users, projects, documents, jobs, audit
MinIO S3-compatible blob storage for uploaded documents
RabbitMQ Asynchronous job queue with dead-letter exchange
Redis Idempotency tokens and short-lived caches
OpenSearch Vector + keyword index for RAG chunks
ClamAV On-upload malware scanning (fail-closed by default in production)
Microsoft Entra OIDC identity provider for staff/users
Google OAuth Secondary identity provider

Data flow: upload-to-answer

User ──upload──▶ WebApp ──scan──▶ ClamAV
                    │
                    ├── blob ──▶ MinIO
                    │
                    ├── metadata ──▶ SQL Server
                    │
                    └── ParseDocumentJob ──▶ RabbitMQ ──▶ Worker
                                                            │
                                    extract + chunk + embed │
                                                            ▼
                                                        OpenSearch
                                                            │
User ──query──▶ WebApp ──RagRetrievalService──▶ OpenSearch ─┘
                    │
                    └── LLM client (Claude / OpenAI / Gemini) ──▶ answer

The step-by-step breakdown lives in RAG Pipeline.

Domain model at a glance

  • Tenant — the top-level isolation boundary. Users belong to tenants through UserTenant with a TenantRole.
  • Workspace — a unit of project grouping within a tenant. Workspaces define default LLM models.
  • Project — contains documents, prompts, chat messages. Projects can override the workspace's default chat/RAG/OCR LLM models and have an optional lifetime (auto-expires).
  • Document — a file uploaded to a project; transitions through states Pending → Processing → Ready | Quarantined | Rejected | Failed.
  • DocumentChunk — the indexed text fragment produced by the RAG pipeline; stored both in SQL and OpenSearch.
  • Job — a persisted record of a background job execution (type, state, attempt count, last error).
  • LlmConnection / LlmModel — shared LLM catalog. Hosted connections are managed by admins and can expose OpenAI-compatible/local endpoints; Personal connections are user BYO credentials for managed cloud providers, AES-256-GCM encrypted at rest.
  • Subscription / PlanLimit — subscription status + enforced plan caps (projects, documents, LLM features).
  • AuditEvent — append-only record of security-relevant events.

See Storage and Persistence for the full schema.

Entry points

  • src/Harpyx.WebApp/Program.cs — WebApp bootstrap (ConfigureHarpyxHostAddHarpyxWebAppServices → pipeline → DB init).
  • src/Harpyx.Worker/Program.cs — Worker bootstrap (ConfigureHarpyxHostAddHarpyxWorkerServices).
  • DI composition: WebApplicationBuilderExtensions, HostApplicationBuilderExtensions, and ServiceCollectionExtensions under Harpyx.Infrastructure/Extensions.

Clone this wiki locally