{"id":51433162,"url":"https://github.com/andregoepel/app-foundation","last_synced_at":"2026-07-05T05:03:45.302Z","repository":{"id":357549879,"uuid":"1113145682","full_name":"andregoepel/app-foundation","owner":"andregoepel","description":"Opinionated .NET 10 application foundation with Blazor, Marten, Wolverine and modern authentication (Passkey, OTP, password)","archived":false,"fork":false,"pushed_at":"2026-07-04T16:13:22.000Z","size":2249,"stargazers_count":2,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-04T16:15:34.871Z","etag":null,"topics":["authentication","blazor","csharp","dotnet","dotnet-10","marten","passkey","wolverine"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andregoepel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"audit/audit-prompt.md","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-09T15:26:48.000Z","updated_at":"2026-07-04T16:13:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/andregoepel/app-foundation","commit_stats":null,"previous_names":["andregoepel/app-foundation"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/andregoepel/app-foundation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andregoepel%2Fapp-foundation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andregoepel%2Fapp-foundation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andregoepel%2Fapp-foundation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andregoepel%2Fapp-foundation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andregoepel","download_url":"https://codeload.github.com/andregoepel/app-foundation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andregoepel%2Fapp-foundation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35143815,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","blazor","csharp","dotnet","dotnet-10","marten","passkey","wolverine"],"created_at":"2026-07-05T05:03:43.949Z","updated_at":"2026-07-05T05:03:45.288Z","avatar_url":"https://github.com/andregoepel.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# app-foundation\n\nA reusable **backend + management-frontend foundation** for .NET 10 / Blazor apps,\npublished as NuGet packages. A consuming app references the packages and gets\nauthentication, user/role administration, a Blazor management shell, durable email,\nand .NET Aspire service defaults out of the box — then adds its own pages and branding\non top.\n\n- **Reference consumer:** [andregoepel.dev](https://github.com/andregoepel/andregoepel-dev)\n- **Identity** is provided by the companion [AndreGoepel.Marten.Identity](https://github.com/andregoepel/marten-identity) packages.\n\n\u003e This repository used to host both the foundation *and* a website. The website has been\n\u003e extracted to its own repo; this repository is now purely the published packages.\n\n---\n\n## Packages\n\n| Package | Purpose |\n|---|---|\n| `AndreGoepel.AppFoundation` | Razor Class Library — the management frontend: layout, navigation, setup, dashboard, error pages. Brand/extend via `AppFoundationLayoutOptions`. |\n| `AndreGoepel.AppFoundation.Hosting` | **Umbrella backend seam** — `AddAppFoundation` / `UseAppFoundation`. Transitively pulls in the other three packages + `AndreGoepel.Marten.Identity.Blazor`. |\n| `AndreGoepel.AppFoundation.MailService` | Email via a Wolverine handler + MailKit SMTP, backed by a durable Marten outbox. |\n| `AndreGoepel.AppFoundation.ServiceDefaults` | .NET Aspire service defaults: OpenTelemetry, health checks, HTTP resilience, service discovery. |\n\nAll four are published to NuGet with lockstep versioning. A host typically references\n`AndreGoepel.AppFoundation.Hosting` (for the wiring) and `AndreGoepel.AppFoundation` (for\nthe UI components it renders directly).\n\n---\n\n## Stack\n\n| Layer | Technology |\n|---|---|\n| Frontend | Blazor (.NET 10), [Radzen](https://blazor.radzen.com/) components |\n| Backend | ASP.NET Core (.NET 10) |\n| Persistence | [Marten](https://martendb.io/) (PostgreSQL document + event store) |\n| Messaging / CQRS | [Wolverine](https://wolverine.netlify.app/) (durable outbox) |\n| Email | [MailKit](https://github.com/jstedfast/MailKit) |\n| Orchestration | [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) |\n| Authentication | ASP.NET Core Identity, event-sourced ([marten-identity](https://github.com/andregoepel/marten-identity)) |\n\n---\n\n## Using it in a host app\n\nReference the packages (versions are centrally managed in your repo):\n\n```xml\n\u003cPackageReference Include=\"AndreGoepel.AppFoundation.Hosting\" /\u003e\n\u003cPackageReference Include=\"AndreGoepel.AppFoundation\" /\u003e\n\u003cPackageReference Include=\"AndreGoepel.Marten.Identity.Blazor\" /\u003e\n```\n\nWire the foundation in `Program.cs`:\n\n```csharp\nbuilder.AddAppFoundation(options =\u003e\n{\n    options.DatabaseConnectionName = \"appfoundation-database\"; // default\n    // options.WolverineServiceName, options.SecretsDirectory ...\n});\n\nbuilder.Services.AddRazorComponents().AddInteractiveServerComponents();\n\n// Brand the management shell and contribute your own admin menu entries.\nbuilder.Services.Configure\u003cAppFoundationLayoutOptions\u003e(o =\u003e\n{\n    o.BrandName = \"your.app\";\n    o.Copyright = \"your.app © 2026\";\n    o.AdminMenu = typeof(YourAdminMenu); // optional Razor component\n});\n\nvar app = builder.Build();\n\napp.UseAppFoundation();\n\napp.MapStaticAssets();\napp.MapRazorComponents\u003cApp\u003e() // your root App.razor\n    .AddInteractiveServerRenderMode()\n    .AddAdditionalAssemblies(\n        typeof(AppFoundationLayoutOptions).Assembly,                         // AppFoundation UI\n        typeof(AndreGoepel.Marten.Identity.Blazor.Initialization).Assembly   // identity pages\n    );\n\napp.MapAdditionalIdentityEndpoints();\n```\n\nThe host owns its root `App.razor` and `Routes.razor`; the router includes the AppFoundation\nand Marten.Identity.Blazor assemblies and uses AppFoundation's `MainLayout` for the\nauthenticated management area. See [andregoepel.dev](https://github.com/andregoepel/andregoepel-dev)\nfor a complete, working example.\n\n---\n\n## Try it locally (sample host)\n\nThe repo ships a runnable example under `samples/`: a .NET Aspire AppHost that starts\nPostgreSQL in a container plus a minimal Blazor Server host that wires the packages exactly\nas [Using it in a host app](#using-it-in-a-host-app) describes. It doubles as a manual smoke\ntest for the foundation.\n\n**Prerequisites:** the .NET 10 SDK and a container runtime (Docker / Podman) for the database.\n\n```bash\ndotnet run --project samples/AndreGoepel.AppFoundation.AppHost\n```\n\nOpen the Aspire dashboard URL printed to the console, start the **web** resource, and open it.\nThe first visit funnels you to **/Setup** to create the administrator; after that you can sign\nin and explore the dashboard and the Administration area.\n\n| Project | Role |\n|---|---|\n| `samples/AndreGoepel.AppFoundation.AppHost` | Aspire orchestrator — starts Postgres and the web app, wiring the `appfoundation-database` connection string |\n| `samples/AndreGoepel.AppFoundation.Sample` | Minimal Blazor Server host consuming the packages — the reference `Program.cs`, `App.razor`, and `Routes.razor` |\n\n---\n\n## Configuration\n\nA PostgreSQL connection string is required, by default under\n`ConnectionStrings:appfoundation-database`.\n\n**`AppFoundationOptions`** (backend seam):\n\n| Option | Default | Purpose |\n|---|---|---|\n| `DatabaseConnectionName` | `appfoundation-database` | Connection-string name to read |\n| `WolverineServiceName` | `AppFoundation` | Service name for the durable inbox/outbox |\n| `SecretsDirectory` | `/run/secrets` | Key-per-file secrets directory (see below) — *from 1.1.0* |\n| `SchemaCreation` | env-based | Marten schema mode; `null` ⇒ `All` in Development, `CreateOrUpdate` otherwise. Set `None` for least-privilege — see [Database schema](#database-schema) |\n| `KnownProxyNetworks` / `KnownProxies` | *(empty)* | Trusted reverse-proxy CIDRs / IPs for `X-Forwarded-*` — see [Running behind a reverse proxy](#running-behind-a-reverse-proxy) |\n| `ConfigureForwardedHeaders` | — | Callback for full `ForwardedHeadersOptions` control |\n| `DataProtectionApplicationDiscriminator` | `WolverineServiceName` | Isolates protected payloads from other apps sharing infrastructure |\n| `ConfigureDataProtection` | — | Callback on the DataProtection builder (Key Vault, cert rotation, …) |\n| `AllowUnprotectedKeyRing` | `false` | Accept an unencrypted key ring outside Development — see [Data protection keys](#data-protection-keys) |\n\n**`AppFoundationLayoutOptions`** (management shell branding):\n`BrandName`, `LogoPath`, `Copyright`, and `AdminMenu` (a Razor component type rendered as\nextra administrator nav entries).\n\n### Docker secrets (from 1.1.0)\n\nConfiguration — notably the connection string — can be supplied as files instead of\nplaintext environment variables. `AddAppFoundation` loads `SecretsDirectory` (default\n`/run/secrets`) key-per-file with `optional: true` (a no-op when absent, so local\ndevelopment is unaffected). A secret named `ConnectionStrings__appfoundation-database`\n(content = the full connection string; `__` maps to the `:` section separator) is read\nstraight into configuration and never enters the process environment.\n\n```yaml\nservices:\n  app:\n    image: ghcr.io/you/your-app:latest\n    environment:\n      - ASPNETCORE_ENVIRONMENT=Production\n    secrets:\n      - ConnectionStrings__appfoundation-database\n\nsecrets:\n  ConnectionStrings__appfoundation-database:\n    file: ./secrets/connectionstring   # chmod 600; or `external: true` under Swarm\n```\n\n---\n\n## Production configuration\n\nOutside `Development`, a few things must be configured — the app is secure by default and\n**fails fast** rather than running in an unsafe state. Checklist for a non-Development host\n(`ASPNETCORE_ENVIRONMENT=Production` or `Staging`):\n\n1. **Connection string** — required (see [Configuration](#configuration)).\n2. **Data protection keys** — required, or the app won't start (see below).\n3. **Reverse proxy** — set the trusted proxy networks so client IP/scheme are honored (see below).\n4. **Database schema** — defaults to non-destructive; tighten for least privilege (see below).\n\n### Data protection keys\n\nThe DataProtection key ring is persisted in the same database as the data it protects, so\noutside Development the foundation **refuses to start unless the keys are encrypted at rest**\n— otherwise a database dump would also expose the keys guarding the SMTP password, login\ntokens, and auth cookies. Provide one of:\n\n- **A certificate** via `DataProtection:CertificatePath` (+ `DataProtection:CertificatePassword`),\n  ideally supplied as Docker secrets:\n\n  ```yaml\n  environment:\n    - DataProtection__CertificatePath=/run/secrets/dataprotection_cert\n  secrets:\n    - dataprotection_cert\n    - DataProtection__CertificatePassword\n  ```\n\n- **Key Vault / KMS** via `options.ConfigureDataProtection = b =\u003e b.ProtectKeysWithAzureKeyVault(...)`.\n- Or, only when the database storage is already encrypted at rest by other means, opt out with\n  `options.AllowUnprotectedKeyRing = true`.\n\n### Running behind a reverse proxy\n\nThe app trusts the TCP peer of each request — which behind a proxy is the **proxy's** address,\nnot the client's. Declare the proxy so `X-Forwarded-For` / `X-Forwarded-Proto` are honored only\nfrom it (and never spoofable by arbitrary clients). Configure via code or, since the production\nCIDR is usually only known at deploy time, via configuration:\n\n```yaml\n# docker-compose.yml — app service\nenvironment:\n  - AppFoundation__KnownProxyNetworks=172.28.0.0/16   # your proxy/ingress subnet\nexpose: [\"8080\"]        # not published to the host — only the proxy can reach the app\nnetworks: [edge]\nnetworks:\n  edge:\n    ipam:\n      config: [{ subnet: 172.28.0.0/16 }]\n```\n\n`AppFoundation__KnownProxyNetworks` (and `AppFoundation__KnownProxies` for single IPs) accept a\ncomma/semicolon/space-delimited list or a config array; IPv6 CIDRs are supported. With none set,\nforwarded headers are trusted from any origin in Development but only from loopback otherwise\n(the app logs a warning). The proxy must forward the headers — for nginx, and to keep Blazor\nServer circuits alive over WebSockets:\n\n```nginx\nlocation / {\n    proxy_pass http://app:8080;\n    proxy_set_header Host              $host;\n    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;   # https → app sees IsHttps=true\n    proxy_http_version 1.1;                        # WebSocket upgrade for SignalR\n    proxy_set_header Upgrade    $http_upgrade;\n    proxy_set_header Connection $connection_upgrade;\n}\n```\n\nFor multiple proxies (e.g. Cloudflare → nginx), also raise `ForwardLimit` via\n`ConfigureForwardedHeaders` and trust each hop.\n\n### Database schema\n\n`SchemaCreation` defaults to `AutoCreate.All` in Development (permits destructive rebuilds) and\n`AutoCreate.CreateOrUpdate` (additive only — never drops) elsewhere, so a code/database mismatch\ncan't destroy data at runtime. For a least-privilege deployment, set `SchemaCreation = AutoCreate.None`\nand provision the schema out-of-band (a migration job / `db-apply`) with a privileged role, then\nrun the app with a role that has no DDL rights.\n\n---\n\n## Repository layout\n\n```\nsrc/\n  AndreGoepel.AppFoundation/             # management-frontend RCL\n  AndreGoepel.AppFoundation.Hosting/     # umbrella backend seam\n  AndreGoepel.AppFoundation.MailService/ # Wolverine + MailKit email\n  AndreGoepel.AppFoundation.ServiceDefaults/\ntests/\n  AndreGoepel.AppFoundation.Tests/\n  AndreGoepel.AppFoundation.MailService.Tests/\n```\n\n---\n\n## Build \u0026 release\n\n```bash\ndotnet restore     # --locked-mode in CI\ndotnet build -c Release\ndotnet test -c Release\n```\n\n- **Central Package Management** with committed `packages.lock.json`; CI restores in\n  `--locked-mode`, runs a vulnerability gate, and pins actions to commit SHAs.\n- **Publishing:** push a `vX.Y.Z` tag → CI packs all four packages and publishes them to\n  NuGet via OIDC trusted publishing (no stored API key).\n\n---\n\n## Architecture notes\n\n**Why Marten?** PostgreSQL as a document store removes the ORM mapping layer for most use\ncases while keeping relational queries available, with event sourcing built in. No separate\nNoSQL infrastructure.\n\n**Why Wolverine?** Clean handler dispatch with built-in message persistence, retries and\noutbox support — async messaging from day one, not bolted on later.\n\n**Why .NET Aspire?** Local orchestration and service discovery that map cleanly to cloud\ndeployment targets.\n\n**Why a modular monolith?** Modules are separated by namespace and handler boundary, not by\nnetwork boundary. Splitting later is possible; splitting prematurely adds operational\noverhead before there's a scaling problem that justifies it.\n\n---\n\n## License\n\nMIT — use freely, attribution appreciated but not required.\n\n---\n\n*Built by [André Göpel](https://andregoepel.dev) — Senior Web Engineer · .NET \u0026 Blazor*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandregoepel%2Fapp-foundation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandregoepel%2Fapp-foundation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandregoepel%2Fapp-foundation/lists"}