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.
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.
Solid green border is the only public surface. Dashed border is internal ingress. Simplified from the ADRs, not a complete deployment diagram.
Key decisions
.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.