Architecture

flowchart LR
    browser[Browser] -->|HTTPS| web[Web app<br/>ASP.NET Core MVC]
    web -->|EF Core| sql[(Azure SQL)]
    web -->|LinkSubmitted| queue[[Service Bus<br/>links queue]]
    queue -->|trigger| function[Summarizer<br/>Azure Function]
    function -->|fetch| page[Web page]
    function -->|IChatClient| claude[Claude Haiku]
    function -->|ExecuteUpdate| sql
    web -->|HTTP API| mailgun[Mailgun]
    web -. secrets .-> vault[Key Vault]
    function -. secrets .-> vault

Projects

Project Role
Digest.AppHost Defines every resource. Runs them locally (SQL Server and the Service Bus emulator in Docker) and generates the Azure infrastructure.
Digest.ServiceDefaults OpenTelemetry, health checks, service discovery and HTTP resilience shared by every service. Also the app's own spans and metrics.
Digest.Contracts The LinkSubmitted message and the dashboard command paths.
Digest.Data DigestDbContext (Identity, links and invites), the entities, migrations and shared size limits.
Digest.Web MVC app styled with Tailwind CSS and daisyUI: invite-only Identity login, the digest, invites, and the CoreAdmin panel at /admin. Applies migrations and creates the admin account on startup.
Digest.Functions The SummarizeLink function: safe fetch, text extraction, Claude, validation.

Request flow

  1. The user saves a link. LinkService stores it as Pending, then publishes LinkSubmitted to the links queue.
  2. SummarizeLink receives the message, fetches the page through the SSRF-safe fetcher, and extracts its text with AngleSharp.
  3. The page text goes to Claude Haiku through IChatClient. The reply must match the LinkSummary shape and size limits.
  4. The function saves the summary, tags, title and reading time with ExecuteUpdateAsync and marks the link Summarized.
  5. A permanent problem (page not found, blocked address, invalid summary) marks the link Failed with a reason.
  6. A transient problem (timeout, 429, 5xx) throws, so Service Bus redelivers the message. If the third attempt also fails, the link is marked Failed and the message is dead-lettered.

Reading time is computed from the word count (230 words per minute), not asked of the model.

Database migrations

The web app applies pending migrations when it starts and creates the Admin role and admin account. EF Core 9 and later hold a database-wide lock while migrating. Deploys need no separate migration step.

Telemetry

Everything is OpenTelemetry, viewed in the Aspire dashboard locally and in Azure.

Signal Source
Spans SubmitLink, FetchPage, SummarizeLink (tagged link.id), plus ASP.NET Core, EF Core, Service Bus and chat spans for each Claude call
Metrics links.submitted, links.summarized, links.failed, links.processing.duration, and the gen_ai token metrics
Logs Source-generated LoggerMessage logs with the link id

Health checks and static files such as CSS are not traced. In Azure every replica of an app reports as one instance, so the dashboard shows web and summarizer.

Prompts and responses are recorded only in Development, where the dashboard's GenAI view shows each conversation.