← Back to ProjectsXavier A. Goss

Virtual Leaders Guide: Architecture Highlights

An event-management platform for Scouting events. Directors and admins build an event's guide in a dashboard, and attendees open it with a shared passcode. It is in active development, and every decision below is recorded as an architecture decision record (ADR) in the public repository.

.NET 10 LTS.NET AspireBlazor ServerEF CoreAzure SQLJSON:APIAzure Container AppsxUnit · bUnit · Playwright

System shape

Two deployable services behind one public door. The browser only ever talks to the Blazor Server app; the API is not reachable from the internet.

Topology diagram The browser connects to the public Blazor Server Web app. Web calls the internal-only API with an internal key and a short-lived JWT. The API reads and writes Azure SQL. Both apps keep Data Protection keys in Blob Storage, and Web sends email through Azure Communication Services. BrowserDirector / visitor WebBlazor Server (public)Identity · passcode gate ApiJSON:API (internal only)EF Core Azure SQLServerless Blob Storageseparate key ring per app Azure Comm. Servicesinvite and reset email key + JWT

Solid green border is the only public surface. Dashed border is internal ingress. Simplified from the ADRs, not a complete deployment diagram.

Key decisions

ADR-0001 · ADR-0034

.NET 10 LTS, Aspire, Blazor and EF Core

Built on the long-term-support framework so a personal project is not forced into upgrades just to stay supported. Aspire orchestrates the solution, service discovery and local dev dashboard included.

Trade-off: the scaffold's Bootstrap was a template default, never a choice. A later ADR replaced it with Radzen components.

ADR-0002 · ADR-0015

Internal-only API, one public surface

Blazor Server is the sole public surface and the API sits behind internal ingress. A shared-secret header, validated by an authentication handler, adds defense in depth against an ingress misconfiguration.

Trade-off: no public API attack surface and no dual-auth API, at the cost that the API can't serve a separate public client without revisiting this boundary.

ADR-0004

REST generated from the model with JsonApiDotNetCore

Evaluated OData and scaffolded controllers, then chose JsonApiDotNetCore: marking an EF Core entity as a resource yields CRUD with filter, sort, page and include, with no per-entity controllers.

Trade-off: the library dictates conventions, such as identifiable entities and explicitly exposed attributes.

ADR-0031

Authorization lives in a resource definition

Admins have full access, while Directors see and edit only the events they are assigned to. That scoping is enforced in a JsonApiDotNetCore resource definition that plugs into the generated pipeline, so the "no hand-written controllers" goal still holds.

Trade-off: a request outside a caller's access returns 403 rather than 404, which confirms the resource exists but follows the acceptance criteria.

ADR-0007

Short-lived JWT to forward identity

Web mints a short-lived token carrying the user's id and every role, including event-scoped ones. The API validates it with standard JWT bearer middleware and authorizes from claims alone, with no database lookup per request.

Trade-off: grants and revocations can take up to about five minutes to reach an open session. Chosen deliberately over re-querying on every call.

ADR-0003 · 0009 · 0026 · 0058

Passcode gate for attendees, encrypted at rest

Attendees need read access, not accounts. A shared per-event passcode unlocks the guide through a purpose-isolated Data Protection cookie rather than a second auth scheme. The passcode is stored encrypted rather than hashed so a director can re-display it, and the API owns that encryption with its own key ring.

Trade-off: encryption protects against a raw database dump while staying recoverable by the app. Separate key rings per app shrink the blast radius.

ADR-0019 · ADR-0020

Own the account lifecycle with ASP.NET Core Identity

Early plans used Entra ID. For a small, self-contained population of volunteer directors, requiring a Microsoft account was the bigger cost, so sign-in moved to local Identity with email and password, and the app sends invites and resets through Azure Communication Services.

Trade-off: the app now owns password storage and email delivery.

ADR-0005 · ADR-0016

Hosting built for near-zero idle cost

Azure Container Apps on the consumption plan, Azure SQL serverless on the free offer, and images in GitHub Container Registry. The Container Apps are created once, so the public/internal ingress split is set in an auditable step rather than re-asserted on every deploy.

Trade-off: a known Aspire limitation around container registries meant provisioning part of the environment outside Aspire's default flow.

ADR-0043

Times stored in UTC, shown in the viewer's timezone

Event start and end times normalize to UTC on assignment and render in each viewer's own browser timezone, so nobody converts by hand.

ADR-0011 · 0014 · 0041 · 0075

Layered testing

Separate test projects for the API, the Aspire AppHost, Blazor components (bUnit) and end-to-end flows (Playwright). A failed end-to-end test is rerun once with a trace and video for evidence.

About 76 ADRs record the reasoning behind this project, including decisions that were later superseded. The full set and the source are on GitHub. The app itself requires sign-in.