Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/components/landing/TechStack.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import SectionEyebrow from './SectionEyebrow.astro';

const stack = [
{ name: '.NET 10', version: 'C# latest', sub: 'net10.0 LTS', icon: 'lucide:hexagon' },
{ name: 'Aspire', version: '13.3', sub: 'orchestration', icon: 'lucide:layers' },
{ name: 'EF Core', version: '10.0.8', sub: 'PostgreSQL 17', icon: 'lucide:database' },
{ name: 'Aspire', version: '13.6', sub: 'orchestration', icon: 'lucide:layers' },
{ name: 'EF Core', version: '10.0.14', sub: 'PostgreSQL 18', icon: 'lucide:database' },
{ name: 'Mediator', version: '3.0', sub: 'source generator', icon: 'lucide:radio' },
{ name: 'FluentValidation', version: '12.1', sub: 'request validation', icon: 'lucide:shield-check' },
{ name: 'FluentValidation', version: '12.2', sub: 'request validation', icon: 'lucide:shield-check' },
{ name: 'Hangfire', version: '1.8', sub: 'background jobs', icon: 'lucide:zap' },
{ name: 'Valkey', version: '8', sub: 'distributed cache', icon: 'lucide:server' },
{ name: 'Valkey', version: '9', sub: 'distributed cache', icon: 'lucide:server' },
{ name: 'Serilog', version: '4.x', sub: 'structured logs', icon: 'lucide:scroll-text' },
{ name: 'OpenTelemetry', version: '1.15', sub: 'OTLP exporter', icon: 'lucide:activity' },
{ name: 'Scalar', version: '2.14', sub: 'OpenAPI 3.1 browser',icon: 'lucide:book-open' },
{ name: 'Finbuckle', version: '10.0', sub: 'multitenancy', icon: 'lucide:building-2' },
{ name: 'OpenTelemetry', version: '1.20', sub: 'OTLP exporter', icon: 'lucide:activity' },
{ name: 'Scalar', version: '2.18', sub: 'OpenAPI 3.1 browser',icon: 'lucide:book-open' },
{ name: 'Finbuckle', version: '10.1', sub: 'multitenancy', icon: 'lucide:building-2' },
{ name: 'React + Vite', version: 'TS 5.7', sub: 'admin · dashboard', icon: 'lucide:code' },
];
---
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/Testing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const pillars: Pillar[] = [
</div>

<pre class="overflow-x-auto p-5 font-mono text-[12px] leading-[1.75] tabular-nums sm:p-6 sm:text-[12.5px]"><code><span class="text-primary">$</span> dotnet test
<span class="text-muted-foreground/80"> ⟳ Testcontainers - postgres:17 · valkey ready</span>
<span class="text-muted-foreground/80"> ⟳ Testcontainers - postgres:18 · valkey ready</span>
<span class="text-primary"> ✓</span> unit ........................ <span class="text-foreground">774 passed</span>
<span class="text-primary"> ✓</span> integration <span class="text-muted-foreground/70">(real Postgres)</span> . <span class="text-foreground">661 passed</span>
<span class="text-primary"> ✓</span> architecture <span class="text-muted-foreground/70">(NetArchTest)</span> . <span class="text-foreground">49 passed</span>
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/building-blocks/mailing.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Mailing building block
lastUpdated: 2026-10-02
lastUpdated: 2026-11-03
description: SMTP or SendGrid email abstraction behind a single IMailService, with multi-recipient delivery and HTML body support.
sidebar:
label: Mailing
Expand Down Expand Up @@ -140,7 +140,7 @@ An unknown name (a typo such as `Plain`) makes the options fail to bind, so the

### Local mail catcher (Docker Compose)

`deploy/docker/docker-compose.yml` reads the SMTP target from `.env` and ships a [Mailpit](https://mailpit.axllent.org) service (`axllent/mailpit:v1.31.3`) under the `mail-catcher` compose profile. The defaults in `.env.example` enable that profile and point the API at it, so confirmation, password-reset and welcome e-mails work without an SMTP account, and none of them is delivered:
`deploy/docker/docker-compose.yml` reads the SMTP target from `.env` and ships a [Mailpit](https://mailpit.axllent.org) service (`axllent/mailpit:v1.33.0`) under the `mail-catcher` compose profile. The defaults in `.env.example` enable that profile and point the API at it, so confirmation, password-reset and welcome e-mails work without an SMTP account, and none of them is delivered:

```bash
# .env
Expand Down
1 change: 1 addition & 0 deletions src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Notable changes to the kit, newest first.

## 2026-11-03

- **Dependencies: the backend stack is on the latest stable versions (chore).** ASP.NET Core, EF Core and `Microsoft.Extensions.*` move to the **10.0.14** patch line (with the `System.Security.Cryptography.Xml` / `.Pkcs` pins), .NET **Aspire 13.6.4** (hosting packages and AppHost SDK), Finbuckle.MultiTenant 10.1.6, FluentValidation 12.2.0, OpenTelemetry 1.20.1 (EF Core and StackExchange.Redis instrumentation on 1.20.1-beta.1), StackExchange.Redis 3.4.31, Scalar.AspNetCore 2.18.7, MailKit/MimeKit 4.18.2, AWSSDK.S3 4.0.104.8, QuestPDF 2026.9.1, Testcontainers 4.16.0 and Spectre.Console.Cli 0.57.2. `global.json` now asks for SDK `10.0.400` or later (still `latestFeature`). Containers: Postgres is **18** everywhere (Docker Compose already was; the Testcontainers harness and the CI migrator smoke move from 17), Valkey 9.1.3, RustFS 1.0.4, Mailpit v1.33.0, `amazon/aws-cli` 2.39.3, and RedisInsight is pinned to 3.8.0 instead of `latest`. Aspire 13.6 still defaults to `postgres:18.3`, so an Aspire data volume from 13.4 or later keeps working; only a volume created before 13.4 needs the [wipe](/docs/getting-started/troubleshooting/). Terraform's AWS provider constraint moves to `~> 6.71` (Terraform 1.15.4 is still the floor). If you forked the CLI, Spectre.Console.Cli 0.57 made `ExecuteAsync` overrides `public`. Part of [#1453](https://github.com/fullstackhero/dotnet-starter-kit/issues/1453); see PR [#1460](https://github.com/fullstackhero/dotnet-starter-kit/pull/1460).
- **Frontends: i18n follow-ups (fix).** The admin language switcher now saves with `If-Match`, so a profile edit made in between is no longer silently overwritten (it retries once on `412`). The dashboard container now honours `FSH_DEFAULT_LANGUAGE` like the admin one; before, a containerized dashboard always started in `en-US`. `deploy/docker` and Terraform pass the setting to both apps. The dashboard no longer matches the English text of the deactivated-tenant error, because the API always sends its `code` now. See [Internationalization](/docs/cross-cutting-concerns/internationalization/#per-deployment-default) and [#1459](https://github.com/fullstackhero/dotnet-starter-kit/pull/1459).
- **Identity: a role can no longer hold the same permission twice (fix, migration).** `identity.RoleClaims` had no uniqueness, so two API instances starting at once after a release that adds a permission, or the tenant seeder overlapping the startup sync, could both insert the same grant and leave a duplicate row behind with nothing in the logs. Authorization was not affected, but the table drifted. The new `AddRoleClaimsUniqueIndex` Identity migration first deletes existing duplicates (keeping the lowest `Id` per `(RoleId, ClaimType, ClaimValue)`) and then adds a unique index on those columns, which replaces the `RoleId`-only index. `RolePermissionSyncer` and `IdentityDbInitializer` treat a unique violation on that index as another writer having inserted the grant first and log it at Information instead of failing, and `PUT /api/v1/identity/{id}/permissions` collapses a permission repeated in the request. Run the DbMigrator `apply` as usual; claims with a `NULL` value are not affected. See [Authorization](/docs/security/authorization/#the-role-permission-syncer), [#1435](https://github.com/fullstackhero/dotnet-starter-kit/issues/1435) and [#1458](https://github.com/fullstackhero/dotnet-starter-kit/pull/1458).
- **Admin & dashboard: sign-in now asks for the authenticator code when two-factor is on (fix).** Both apps let a user enable two-factor authentication under Settings > Security, but neither login page could send the code, so the next sign-in failed with `401 two_factor_required` and locked the user out. When `POST /api/v1/identity/token/issue` answers with the ProblemDetails `code` `Identity.TwoFactorRequired`, the login card now switches to a code step (six digits, `autocomplete="one-time-code"` so the browser or password manager can fill it), then repeats the call with `twoFactorCode`. A wrong code (`Identity.TwoFactorInvalid`) shows an inline error and keeps the user on the step; "Back to sign in" returns to the password step. The password is held only in the login page's memory for the retry, never in browser storage, and is dropped on back, success or leaving the page. The clients branch on the `code` extension, not the localized `detail` text. Recovery codes are still not supported. See [Two-factor authentication](/docs/security/two-factor/#signing-in-from-the-react-apps) and [#1446](https://github.com/fullstackhero/dotnet-starter-kit/issues/1446).
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/compare/fsh-vs-abp.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: fullstackhero vs ABP Framework
lastUpdated: 2026-06-11
lastUpdated: 2026-11-03
description: A fair comparison of fullstackhero and ABP Framework for building production .NET 10 applications. License, architecture, learning curve, and when to pick which.
sidebar:
label: vs ABP Framework
Expand Down Expand Up @@ -32,7 +32,7 @@ ABP Framework and fullstackhero both target the same problem - getting a product
| ORM | EF Core 10 (PostgreSQL via Npgsql by default; SQL Server provider available) | EF Core with abstraction layer + LINQ via repository pattern |
| Background jobs | Hangfire 1.8 | ABP's `IBackgroundJobManager` (multiple providers) |
| Realtime | SignalR + Server-Sent Events | SignalR via ABP modules |
| Observability | Serilog 4 + OpenTelemetry 1.19 (OTLP exporter), pre-wired | OpenTelemetry support; configuration via ABP modules |
| Observability | Serilog 4 + OpenTelemetry 1.20 (OTLP exporter), pre-wired | OpenTelemetry support; configuration via ABP modules |
| Frontend included | React + Vite admin and dashboard, in the repo | MVC + Razor / Blazor / Angular options - Lepton theme is paid in ABP Commercial |
| CLI scaffolding | `fsh new <name>` | `abp new <name>` (Studio offers a GUI) |
| Stars (May 2026) | 6.4k★ | 14.2k★ |
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/compare/fsh-vs-blazorplate.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: fullstackhero vs BlazorPlate
lastUpdated: 2026-09-25
lastUpdated: 2026-11-03
description: How fullstackhero compares to BlazorPlate - the difference between a free, open-source .NET 10 starter kit and a paid closed-source SaaS template.
sidebar:
label: vs BlazorPlate
Expand Down Expand Up @@ -30,7 +30,7 @@ BlazorPlate is a paid commercial multi-tenant SaaS starter for .NET, sold as a o
| Frontend | React 19 + Vite admin console + tenant dashboard (both in the repo) | Blazor Server + Blazor WebAssembly |
| i18n | Not built-in | 20+ languages, RTL support |
| Background jobs | Hangfire 1.8 | Hangfire |
| Observability | Serilog 4 + OpenTelemetry 1.19 (OTLP) | Serilog |
| Observability | Serilog 4 + OpenTelemetry 1.20 (OTLP) | Serilog |
| Realtime | SignalR (Valkey backplane) + Server-Sent Events | SignalR |
| File storage | Tenant-scoped S3 abstraction (RustFS locally) | File storage primitives |
| Email | MailKit / SendGrid | Email service |
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/compare/fsh-vs-clean-architecture.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: fullstackhero vs Clean Architecture templates
lastUpdated: 2026-05-19
lastUpdated: 2026-11-03
description: How fullstackhero compares to the Jason Taylor and Ardalis Clean Architecture templates - the difference between shipping an architecture and shipping production modules on top of one.
sidebar:
label: vs Clean Architecture
Expand All @@ -27,7 +27,7 @@ The Clean Architecture templates by Jason Taylor and Steve Smith (Ardalis) are t
| Multitenancy | Not included | Not included | Finbuckle 10 with EF Core global query filter, IGlobalEntity opt-out, tenant-aware cache + jobs + outbox |
| Auditing | Not included | Not included | EF SaveChanges interceptor + per-entity before/after + queryable `/audits` |
| Background jobs | Not included | Not included | Hangfire 1.8 with tenant context preserved |
| Observability | Console logging | Serilog example | Serilog 4 + OpenTelemetry 1.19 (OTLP) pre-wired |
| Observability | Console logging | Serilog example | Serilog 4 + OpenTelemetry 1.20 (OTLP) pre-wired |
| Caching | Not included | Not included | HybridCache (in-memory + Valkey) with tenant-scoped invalidation |
| API browser | Swagger | Swagger | Scalar (OpenAPI 3.1) on `/scalar/` |
| Frontend | Angular 21 / React 19 (sample) | None (API-only) | React 19 + Vite admin + dashboard, in the repo |
Expand Down
9 changes: 5 additions & 4 deletions src/content/docs/deployment/aspire.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Local Orchestration with .NET Aspire
lastUpdated: 2026-09-30
lastUpdated: 2026-11-03
description: What spins up when you run the AppHost, and the decisions behind the local topology - Postgres, Valkey, RustFS, the migrator, the API, and both React apps.
sidebar:
label: Local Orchestration (Aspire)
Expand Down Expand Up @@ -31,7 +31,7 @@ documents what it starts and **why** it's wired the way it is.
| pgAdmin | *(sidecar)* | Web UI on **:5050**, auto-discovers every database on the server | Persistent |
| Valkey | `redis` | Distributed cache (HybridCache L2) + SignalR backplane + Data Protection key ring (**Valkey** - a Redis-compatible, BSD-licensed Redis fork; resource name stays `redis`) | Persistent (data volume) |
| RedisInsight | `redis-insight` | Key browser on **:5540**, pre-connected to the Valkey instance for inspecting cache keys in dev | Persistent |
| RustFS | `rustfs` | S3-compatible object storage (`rustfs/rustfs:1.0.0`), **:9000** (S3 API) / **:9001** (web console) | Persistent (data volume) |
| RustFS | `rustfs` | S3-compatible object storage (`rustfs/rustfs:1.0.4`), **:9000** (S3 API) / **:9001** (web console) | Persistent (data volume) |
| RustFS init | `rustfs-init` | One-shot (`amazon/aws-cli`): creates the `fsh-uploads` bucket + an anonymous `GetObject` policy on `public/*` only (re-applied every run), then exits | Run-once |
| DB migrator | `fsh-starter-db-migrator` | One-shot: applies migrations across the tenant catalog + every tenant's module DBs (`apply --seed`, so the root admin exists), then exits | Run-once |
| Demo seeder | `fsh-starter-demo-seeder` | One-shot, **dev-only**: runs `seed-demo` after the migrator to provision the `acme`/`globex` demo tenants + users, then exits | Run-once |
Expand Down Expand Up @@ -93,7 +93,7 @@ two contexts.
### Valkey for caching and realtime, with a RedisInsight browser

The cache engine is **Valkey** (a Redis-compatible, BSD-licensed Redis fork),
pulled as `valkey/valkey:9.1.0`. It speaks the Redis protocol (RESP), so the
pulled as `valkey/valkey:9.1.3`. It speaks the Redis protocol (RESP), so the
.NET client (`StackExchange.Redis`) and every `CachingOptions:Redis` config key
are unchanged - and the resource keeps the name `redis` in code. It backs the
HybridCache L2, the SignalR backplane, and the Data Protection key ring
Expand All @@ -115,7 +115,8 @@ Keep it that way.

<Callout type="warning" title="A Postgres major bump can strand your data volume">
`AddPostgres("postgres")` doesn't pin an image tag - it tracks whatever the
Aspire hosting package defaults to (PostgreSQL 18 as of Aspire 13.4). When a
Aspire hosting package defaults to (PostgreSQL 18 since Aspire 13.4; Aspire 13.6
still ships `postgres:18.3`, so moving from 13.5 to 13.6 keeps your volume). When a
package update jumps a Postgres **major** version, the persistent
`*-postgres-data` volume written by the older major can't be read and the
container exits. Recovery for local dev data: remove the volume and relaunch -
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/deployment/aws-terraform.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Deploy to AWS with Terraform
lastUpdated: 2026-09-30
lastUpdated: 2026-11-03
description: End-to-end AWS deployment - prerequisites, bootstrapping the state backend, configuring an environment, and the one-command deploy that ships the API and both React apps.
sidebar:
label: AWS (Terraform)
Expand Down Expand Up @@ -69,7 +69,7 @@ ACM cert (in `us-east-1` for CloudFront) and DNS when you're ready.

<Callout type="warning" title="Terraform 1.15.4 is the floor">
The configuration pins `required_version >= 1.15.4` and the AWS provider to
`~> 6.46`. An older CLI will be rejected at `terraform init`. Upgrade with e.g.
`~> 6.71`. An older CLI will be rejected at `terraform init`. Upgrade with e.g.
`choco upgrade terraform` (Windows), `brew upgrade terraform` (macOS), or your
package manager. A committed `.terraform.lock.hcl` keeps provider versions
identical across machines and CI.
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/deployment/ci-cd.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: CI/CD Pipeline
lastUpdated: 2026-06-11
lastUpdated: 2026-11-03
description: The GitHub Actions setup - path-scoped backend and frontend pipelines, a pinned SDK, deduplicated test + coverage, CodeQL, the template smoke guard, and how container images and NuGet packages are published.
sidebar:
label: CI/CD
Expand Down Expand Up @@ -39,7 +39,7 @@ Triggers on pushes to `main`, `v*` tags, and PRs to `main`, plus manual `workflo
| **Detect changes** | - | always | A `dorny/paths-filter` step decides whether backend code changed. On any push/tag/dispatch it's always `true`; on a PR it's `true` only when `src/**`, `global.json`, `coverage.runsettings`, or the workflow itself changed. Every heavy job gates on this. |
| **Unit Tests** | Detect changes | backend changed | `dotnet build -c Release -warnaserror` (the build gate), an audit that **fails on directly-referenced vulnerable packages** (transitive advisories are reported, not blocking), then the 12 unit/architecture test projects **with coverage collection**. Uploads the coverage as an artifact. |
| **Integration Tests** | Detect changes | backend changed | `Integration.Tests` + `Integration.Middleware.Tests` via `WebApplicationFactory` + Testcontainers (Docker on the runner), **with coverage collection**. Uploads its coverage as an artifact. |
| **DbMigrator Container Smoke** | Detect changes | backend changed | Publishes the `fsh-db-migrator` image (Dockerfile-less SDK publish) and runs `apply --catalog-only` against an ephemeral Postgres 17 - asserts it finishes successfully. Catches container-publish and DI-graph regressions. |
| **DbMigrator Container Smoke** | Detect changes | backend changed | Publishes the `fsh-db-migrator` image (Dockerfile-less SDK publish) and runs `apply --catalog-only` against an ephemeral Postgres 18 - asserts it finishes successfully. Catches container-publish and DI-graph regressions. |
| **Coverage Gate** | Unit + Integration | backend changed | **Merges** the coverage artifacts from the two test jobs with ReportGenerator and **fails if line coverage drops below 80%**. It does *not* re-run the tests - a ratchet; raise `MIN_LINE` as coverage grows. |
| **Publish Dev Containers** | Unit + Integration | push to `main` | Publishes the API **and** DbMigrator images to GHCR tagged `dev-<sha>` and `dev-latest`. |
| **Publish Release** | Unit + Integration | `v*` tag, or `workflow_dispatch` on `main` | Packs the `fsh` CLI + the `dotnet new` template, pushes both to NuGet.org, and publishes the API + DbMigrator images to GHCR tagged `<version>` and `latest`. |
Expand Down
6 changes: 3 additions & 3 deletions src/content/docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Introduction
lastUpdated: 2026-09-25
lastUpdated: 2026-11-03
description: What fullstackhero is, what it ships with, and how it's put together.
sidebar:
label: Introduction
Expand Down Expand Up @@ -145,10 +145,10 @@ The API never mutates data on startup. Demo data (acme/globex tenants, sample ca
| Storage | Local filesystem or S3-compatible (AWS SDK) |
| Messaging | RabbitMQ client (optional) |
| Logging | Serilog 4 (structured) |
| Tracing / metrics | OpenTelemetry 1.19 (OTLP exporter) |
| Tracing / metrics | OpenTelemetry 1.20 (OTLP exporter) |
| API docs | OpenAPI 10 + Scalar UI |
| Errors | `ProblemDetails` (RFC 9457) via global exception handler |
| Orchestration | .NET Aspire 13.5 (Postgres + Valkey + RustFS + API + both React apps) |
| Orchestration | .NET Aspire 13.6 (Postgres + Valkey + RustFS + API + both React apps) |

### Frontends

Expand Down
Loading