> ## Documentation Index
> Fetch the complete documentation index at: https://docs.valedo.uniwave.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> Components, layering and the signature hot path.

Valedo is a multi-tenant SaaS. Dependencies point inward only: `Domain` ← `Application` ← `Infrastructure` ← API and Worker hosts.

## Deployables

| Component | Project | Hosting |
| - | - | - |
| Valedo.API (add-in endpoint, admin API, Marketplace webhook) | `src/Valedo.Api` | Azure App Service (Linux) |
| Publisher API (internal console; separate host and token audience) | `src/Valedo.Publisher.Api` | Azure App Service (Linux) |
| Background jobs (directory sync, mailbox counting, metering, tenant lifecycle, purge) | `src/Valedo.Worker` | Azure Functions (isolated) |
| Outlook add-in (`OnNewMessageCompose`) | `src/addin` | Office.js, Marketplace |
| Admin Portal and Publisher Console | `src/web-admin`, `src/web-publisher` | React + Vite SPAs |

## Signature hot path

`GET /api/v1/signature?context=new|reply|forward&from={smtp?}`

Every add-in request follows this path. The hot path never calls Microsoft Graph in steady state and never evaluates raw rule documents; it reads a compiled, immutable snapshot.

1. Validate the Office SSO bearer token (audience = our multi-tenant API, signature, lifetime) → `tid`, `oid`.
2. Look up `tid` in the tenant registry (memory cache, 60 s). Unknown → `403 tenant_not_onboarded`. Not licensed → `402 license_inactive` (body explains; add-in inserts nothing).
3. Load the tenant's current `configSnapshot` (memory cache keyed by snapshot version; invalidated via change feed → Redis pub/sub or short TTL).
4. Load the user's directory document (point read). Missing → fetch from Graph app-only once and upsert (lazy fill).
5. Evaluate rules (README §5) → template version → render placeholders → return HTML + `ETag` + photo/asset references.
6. Add-in caches by `ETag` per session; `If-None-Match` → `304`.

Budget: p95 under 150 ms server time and at most three Cosmos point reads per uncached request.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant AO as Outlook add-in
    participant API as Valedo.API
    participant REG as Tenant registry (cache 60 s)
    participant SNAP as configSnapshot (cache by version)
    participant DIR as Cosmos directory
    participant G as Microsoft Graph (app-only)
    AO->>API: GET /api/v1/signature?context&from (Office SSO bearer)
    API->>API: Validate token, get tid and oid
    API->>REG: Look up tid
    alt Unknown tenant
        REG-->>API: Not onboarded
        API-->>AO: 403 tenant_not_onboarded
    else Not licensed
        REG-->>API: Subscription inactive
        API-->>AO: 402 license_inactive (add-in inserts nothing)
    else Licensed
        API->>SNAP: Load current snapshot
        API->>DIR: Point read user document
        opt Document missing (lazy fill)
            DIR-->>API: Not found
            API->>G: Fetch user profile once
            G-->>API: Profile
            API->>DIR: Upsert user document
        end
        API->>API: Evaluate rules, render template, compute ETag
        alt If-None-Match matches ETag
            API-->>AO: 304 Not Modified
        else
            API-->>AO: 200 HTML + ETag
        end
    end
```

## Data

Cosmos DB for NoSQL. All containers are partitioned by tenant. Per-tenant configuration is small, so related documents share a partition and can be written atomically with a transactional batch. Every document carries `id`, `tenantId`, `type`, `schemaVersion`, `createdAtUtc`, `updatedAtUtc` and `_etag` (optimistic concurrency on all updates).

| Container | Partition key | Documents (`type`) | Notes |
| - | - | - | - |
| `tenants` | `/tenantId` | `tenant`, `tenantSettings`, `consent` | Registry of onboarded tenants; status; feature flags. |
| `config` | `/tenantId` | `template`, `templateVersion`, `rule`, `asset`, `configSnapshot` | Templates are versioned; publishing writes an immutable compiled `configSnapshot` used by the hot path. |
| `directory` | hierarchical `/tenantId`, `/userId` | `user` | Minimal profile cache from Graph + IDs of *rule-referenced* groups. Hierarchical key avoids the 20 GB logical-partition limit for very large tenants. |
| `billing` | `/tenantId` | `subscription`, `mailboxSnapshot`, `usageReport`, `pricingOverride` | Marketplace subscription mirror, daily counts with evidence, idempotent usage records. |
| `audit` | `/tenantId` | `auditEvent` | Append-only; TTL per retention policy. |
| `publisher` | `/kind` | `pricing`, `plan`, `webhookEvent` | Publisher-wide documents (small). |
| `leases` | `/id` | — | Change-feed processor leases. |

### Container access by project

Database `signatures` (one Cosmos DB account, serverless). Each host authenticates to Cosmos DB with its own managed identity (`DefaultAzureCredential`, no keys). Which repositories a host registers decides what it can reach: `AddInfrastructure` is shared; `AddPublisherInfrastructure` runs only in the Publisher API; `AddMarketplaceWebhookInfrastructure` runs in the Signature API and the Worker; `AddLifecycleInfrastructure` runs only in the Worker. Solid arrows exist in code today; dotted arrows are planned (README §12).

```mermaid theme={null}
flowchart LR
    subgraph Hosts
        API["Valedo.Api<br/>Signature API"]
        PUB["Valedo.Publisher.Api<br/>Publisher Console backend"]
        WRK["Valedo.Worker<br/>Azure Functions"]
    end

    subgraph DB["Cosmos DB database: signatures"]
        T[("tenants<br/>/tenantId<br/>tenant")]
        C[("config<br/>/tenantId<br/>template, rule")]
        D[("directory<br/>/tenantId, /userId<br/>user")]
        B[("billing<br/>/tenantId<br/>subscription, mailboxSnapshot, usageReport")]
        A[("audit<br/>/tenantId<br/>auditEvent")]
        P[("publisher<br/>/kind<br/>marketplaceEvent")]
        L[("leases<br/>/id")]
    end

    %% Signature API: single-tenant access only, tenant from the validated token
    API -->|"read: tenant registry and licence check (CosmosTenantRepository)"| T
    API -->|"read/write: templates and rules (CosmosTemplateRepository, CosmosRuleRepository)"| C
    API -->|"write: raw webhook events (CosmosMarketplaceEventStore)"| P

    %% Publisher API: the only host with cross-tenant queries
    PUB -->|"read: all tenants, status counts (CosmosPublisherTenantRepository)"| T
    PUB -->|"read: webhook events, never rawBody (CosmosPublisherMarketplaceEventRepository)"| P

    %% Worker: cross-tenant lifecycle and event processing
    WRK -->|"read/write: lifecycle transitions across tenants (CosmosTenantLifecycleRepository)"| T
    WRK -->|"read/write: pending events, retry state (CosmosMarketplaceEventStore)"| P

    %% Planned, no repository yet
    API -.->|"planned: compiled configSnapshot, user point read + lazy fill"| C
    API -.-> D
    WRK -.->|"planned: snapshot-compiler (change feed)"| C
    WRK -.->|"planned: directory-delta-sync"| D
    WRK -.->|"planned: mailbox-count, usage-emitter"| B
    WRK -.->|"planned: tenant-purge audit"| A
    WRK -.->|"planned: change-feed leases"| L
    WRK -.->|"planned: erasure record"| P
```

| Project | Containers today | Access | Notes |
| - | - | - | - |
| `Valedo.Api` | `tenants`, `config`, `publisher` | `tenants` read; `config` read/write; `publisher` write (webhook intake) | Tenant partition key always comes from `TenantContext`. Registers the tenant, template and rule repositories through `AddInfrastructure`. |
| `Valedo.Publisher.Api` | `tenants`, `publisher` | read only | Cross-tenant queries; `AddInfrastructure` also registers the customer repositories, but no Publisher endpoint uses them. |
| `Valedo.Worker` | `tenants`, `publisher` | read/write | `marketplace-webhook-processor` and `tenant-lifecycle` jobs. `mailbox-count` is a skeleton and does not touch Cosmos yet. |
| `Valedo.Infrastructure` | all | defines every repository and `CosmosContainers` | Only project that references the Cosmos SDK. `directory`, `billing`, `audit` and `leases` have constants and infrastructure but no repository yet. |
| `Valedo.Application`, `Valedo.Domain` | none | — | Application depends on repository abstractions only; Domain does no I/O. |

## Background jobs

| Job | Trigger | Notes |
| - | - | - |
| `marketplace-webhook-processor` | Every minute (timer over stored events, README §4.8) | Confirms via Get Operation, resolves the beneficiary with Get Subscription, updates tenant state; idempotent by operation ID; back-off retries, dead-letter after 15 attempts. |
| `directory-delta-sync` | Timer → fan-out queue per tenant | Delta tokens stored per tenant; full resync on token expiry. |
| `mailbox-count` | Daily timer → per-tenant queue | Writes `mailboxSnapshot` with evidence. |
| `usage-emitter` | Hourly timer | Finds terms ending within 24 h or pending cancellation emission; batch API (≤ 25 events). |
| `usage-reconciler` | Daily | `GET usageEvents`, flags `Rejected`/`Mismatch`. |
| `subscription-reconciler` | Daily | Lists subscriptions, fixes drift, alerts. |
| `snapshot-compiler` | Cosmos change feed on `config` | Rebuilds `configSnapshot`, bumps version, invalidates caches. |
| `tenant-lifecycle` | Hourly | Moves tenants from `Transition` to `Retrieval` at `transitionEndsAt` and queues purge at `retrievalEndsAt` (README §4.8). |
| `export-builder` | Service Bus queue | Builds a tenant's export ZIP in Blob (README §4.8), deletes it after 7 days. |
| `tenant-purge` | Daily | Deletes tenant data after `retrievalEndsAt` (at least 60 days after cancellation, README §4.8), keeps billing/audit records required by law, writes an erasure record. |

All jobs: correlation ID per run, structured logs, dead-letter on poison messages, alert on repeated failure. Exceptions are logged once at the job boundary.

## Scaling

Sizing **estimates** (validate with load tests): 1 000 tenants × average 200 mailboxes = 200 000 mailboxes; \~20 compose events per mailbox per working day → \~4 M signature requests/day ≈ 50 rps average, \~500 rps peak. With add-in `ETag` caching most requests become `304` without a render.

| Layer | How it scales | First bottleneck / mitigation |
| - | - | - |
| Valedo.API | Stateless; App Service scale-out on CPU/requests (Premium target: 2 → N instances; B1 interim: single instance); in-memory snapshot cache per instance | Cache misses after publish → versioned snapshots + Redis when instance count grows |
| Cosmos DB | Serverless RU consumption (autoscale RU/s when provisioned); point reads by tenant/user; tenant partitions spread naturally | Hot partition for a huge tenant → hierarchical key on `directory`; monitor normalized RU per partition |
| Graph | Off the hot path; delta queries; per-tenant concurrency limits | Throttling on very large tenants → longer sync interval, backoff |
| Worker | Queue-driven fan-out; Flex Consumption scales per queue length | Marketplace API rate limits → batching (25 events) and jitter |
| Front ends | Static, CDN-cached | — |
| Regions | Start single region (West Europe) with zone redundancy; add a second EU region behind Front Door with Cosmos multi-region reads when SLA requires | Data residency: keep all regions in the EU |

## Solution layout

```text theme={null}
src/
  Valedo.Domain/          entities, value objects, rule evaluator, renderer (no I/O)
  Valedo.Application/     use cases, abstractions (repositories, Graph, Marketplace), licensing logic
  Valedo.Infrastructure/  Cosmos, Blob, Service Bus, Graph (app-only), Marketplace clients
  Valedo.Api/             Minimal API: add-in, customer admin, marketplace landing/webhook
  Valedo.Publisher.Api/   Minimal API for the Publisher Console (separate deployment)
  Valedo.Worker/          Azure Functions isolated worker: timers, queues, change feed
  addin/                  Outlook add-in (TypeScript, esbuild) — evolved from the MVP
  web-admin/              Admin Portal (React, Fluent UI v9)
  web-publisher/          Publisher Console (React, Fluent UI v9)
  web-shared/             shared TS: generated API client, auth helpers, design tokens
tests/                    xUnit per project; Vitest for TS; Playwright for portals
infra/                    Bicep (main + modules), per-environment deployment scripts (shared code in lib/),
                          parameters/ with *.parameters.example.json only
```

## Where the full design lives

The README at the repository root is the blueprint of decisions, assumptions and open questions. Use it for:

* Commercial model and Marketplace integration: README §2 and §4.8
* Directory sync and identity: README §4.6 and §4.7
* Metering: README §4.9
* Add-in distribution: README §4.10, and [Publishing and test deployment](https://docs.valedo.uniwave.ai/developers/publishing.md)
* Security and compliance: README §9
* Delivery plan and open questions: README §12 and §13


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.