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
- The user saves a link.
LinkServicestores it asPending, then publishesLinkSubmittedto thelinksqueue. SummarizeLinkreceives the message, fetches the page through the SSRF-safe fetcher, and extracts its text with AngleSharp.- The page text goes to Claude Haiku through
IChatClient. The reply must match theLinkSummaryshape and size limits. - The function saves the summary, tags, title and reading time with
ExecuteUpdateAsyncand marks the linkSummarized. - A permanent problem (page not found, blocked address, invalid summary) marks the link
Failedwith a reason. - A transient problem (timeout, 429, 5xx) throws, so Service Bus redelivers the message. If the third attempt also fails, the link is marked
Failedand 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.