Notable changes to the kit, newest first.
2026-06-01
Post-10.0.0 pre-release hardening from a module-by-module audit (correctness, security, completeness, test coverage) across the backend. Every finding was adversarially verified before fixing; the suite stays green (warnings-as-errors, Testcontainers integration tests).
- Chat: restoring an archived channel no longer loses its members (fix). Archiving a channel called
db.Remove(channel), which cascaded the delete onto theChannelMemberrows; because the soft-delete interceptor only rescues owned references, the members were hard-deleted and a later restore brought back an empty channel — the creator then got404trying to post. Archiving is now an explicit domain state change that flips the soft-delete flag and leaves membership intact, so a restore is lossless. - Tickets: the lifecycle and permission set are now complete. The
Closedstate was unreachable (no way to get there) and theTickets.Update/Tickets.Deletepermissions were registered with no endpoints behind them — so an admin could grant rights that did nothing, and the existing trash/restore had no way to actually trash a ticket. This adds first-class Close (POST /api/v1/tickets/{id}/close, Resolved → Closed), Update (PUT /api/v1/tickets/{id}— edit title/description/priority; frozen once Closed), and Delete (DELETE /api/v1/tickets/{id}— soft-delete; comments survive and return on restore), each with a validator, a permission gate, and a newTickets.Closepermission.GET /api/v1/tickets/{id}/commentsnow returns404for a non-existent ticket instead of a misleading empty list. See Tickets. - Webhooks: signing secrets are encrypted at rest, and the endpoints are permission-gated (security fixes). The HMAC signing secret was stored as plaintext in a field named
SecretHash— a database breach would have exposed every tenant’s secret. The secret is the HMAC key (it must stay recoverable, so hashing isn’t an option), so it is now encrypted with ASP.NET Data Protection on create and decrypted only at sign time. Separately, the endpoints were authentication-only — any signed-in user could manage every webhook in their tenant; they now require the newWebhooks.View/Create/Delete/Testpermissions. After upgrading, grant these to the roles that manage webhooks or those users will get403. See Webhooks. - Multitenancy & correctness. The
GET tenant provisioning statusandretry provisioningendpoints now accept and forward aCancellationToken(graceful shutdown); the Notifications mention handler fails loud on a tenant-context mismatch rather than risking a cross-tenant write; webhook list endpoints validate pagination (apageSize=0previously surfaced as a500, now a clean400); and the Billing monthly-invoice job takes its clock fromTimeProviderfor deterministic tests. - Known follow-up. Billing publishes its
InvoiceIssuedevent directly on the in-memory bus rather than through the transactional outbox (Files does the same, with no consumer yet). Closing this needs a smallEventingbuilding-block change to support more than one outbox-backed module; tracked for a follow-up release. The in-memory path is correct today.
10.0.0 — 2026-05-28
The first stable 10.0.0 release. fullstackhero is now a complete .NET 10 modular monolith plus two React 19 apps — and you get the full source, no black-box runtime packages. Available today via git clone or the GitHub template; the fsh CLI and the dotnet new fsh template publish to NuGet shortly.
- Backend — .NET 10 / EF Core 10 modular monolith (Vertical Slice + source-generated Mediator CQRS) across 10 modules: Identity, Multitenancy, Billing, Catalog, Tickets, Chat, Files, Webhooks, Auditing, and Notifications. Multitenant by default (Finbuckle), JWT + ASP.NET Identity, HybridCache on Valkey, Hangfire jobs, presigned S3/MinIO storage, OpenAPI + Scalar, and Serilog + OpenTelemetry.
- Front-ends — two React 19 + Vite 7 + TypeScript apps: an operator console (
admin) and a tenant app (dashboard), with TanStack Query v5, Tailwind v4, and SignalR/SSE real-time. - One-command local dev — .NET Aspire brings up Postgres + pgAdmin, Valkey + RedisInsight, MinIO, the migrator, demo data, the API, and both front-ends. Docker Compose and AWS/Terraform cover deployment.
- Tested & enforced — 1,600+ backend tests (xUnit, Testcontainers, NetArchTest boundaries) and 200+ Playwright E2E tests, with path-scoped backend/frontend CI and warnings-as-errors.
- Polish in this release — the
fshCLI gained a--versionflag and a corrected (semver-aware) update check; the unimplemented--db sqlserverscaffold option was removed (PostgreSQL is the supported provider); AppHost resource names are namespaced per app; and a batch of scaffold/DX fixes landed (see the dated entries below).
See the dated entries below for the complete list of changes that shipped into 10.0.0.
2026-05-28
-
Tenant billing is now complete end-to-end — expiry/renewal emails, PDF invoices, and a tenant-facing billing view. Building on the plan-driven subscription/invoice lifecycle, this round finishes the SaaS billing story. A daily Hangfire scan (
tenant-expiry-scan, 02:00 UTC) classifies every active tenant as nearing expiry, in grace, or expired and emails the tenant admin — deduped so each state notifies once per validity window (and re-arms automatically on renewal). Issuing an invoice now also emails the tenant. Invoices are downloadable as PDF (GET /api/v1/billing/invoices/{id}/pdf, QuestPDF behind a swappableIInvoicePdfRenderer); the download is tenant-scoped, so one endpoint safely serves both the operator console and tenant self-service. The dashboard gains a/subscriptionpage (plan, validity, usage, recent invoices), a global expiry/grace warning banner, and invoice detail with PDF download; the admin console gets a PDF button, client-side plan-form validation, and an Adjust validity operator override (POST /tenants/{id}/adjust-validity) that sets a tenant’s expiry directly with no invoice — for comps and corrections. New config keyBilling:ExpiryNotificationLeadDays(default 7). Note: QuestPDF’s Community license is free for organisations under $1M USD/year revenue; larger commercial users must obtain a license — the dependency is isolated behindIInvoicePdfRendererif you prefer to swap it. -
Background-published lifecycle events no longer crash the webhook fan-out (fix). The generic webhook fan-out handles every integration event and reads a tenant-filtered context that captures the ambient tenant at construction — so events published from a background job (no HTTP request) hit a null tenant and threw. Background publishers (the new expiry scan) now install the tenant context before publishing, so the webhook fan-out and email handlers run correctly. The renewal stacking math also now uses the injected clock (was
DateTime.UtcNow), and aX-Subscription-Graceresponse header reports the days left while a tenant is in its grace window. -
Chat delivers messages live to recipients who weren’t in the conversation when they connected — chat broadcasts each message to the channel’s SignalR group, but a connection only joined the groups for channels it already belonged to at connect time (
AppHub.OnConnectedAsync). So a brand-new DM, or being added to a channel mid-session, never received live messages — the recipient saw nothing until they reloaded the page. The hub now exposes a membership-checkedJoinChannelmethod that the dashboard invokes when a conversation is opened and again on reconnect, so a live socket joins the group on demand. Creating a DM also notifies the other participants (via theiruser:{id}group), so the new conversation appears in their channel rail without a refresh. -
Deactivated tenants are now actually blocked (security fix) — deactivating a tenant only flipped an
IsActiveflag in the tenant store; nothing in the auth or request pipeline enforced it, so a deactivated tenant’s users could still log in and use the API. Tenant resolution now rejects requests for a deactivated tenant with403 Forbidden— covering login, token refresh, and every API/realtime request — via a post-authentication guard. Operators (the root tenant) are exempt so they can still manage and reactivate tenants. Deactivation also now invalidates the tenant’s distributed-cache entry, so the change takes effect on the very next request instead of waiting out the 60-minute cache.
2026-05-27
- Dependencies updated to latest for the v10 release — .NET Aspire 13.3.5 (Hosting packages + AppHost SDK), Finbuckle.MultiTenant 10.1.0, MailKit/MimeKit 4.17.0, AWSSDK.S3 4.0.23.4, Scalar.AspNetCore 2.14.14, and SonarAnalyzer 10.27. Builds clean with warnings-as-errors and the full test suite (unit + Testcontainers integration) stays green.
- Template packaging fixes — scaffolded Dockerfiles and dev-machine packing —
dotnet new fsh/fsh newpacked extensionless files (everyDockerfile) to a doubled nested path, so scaffolded projects got aDockerfiledirectory instead of a file anddeploy/docker(docker compose up) was broken. Also made the IDE-cache excludes (.vs/.idea/.vscode) recursive sodotnet packno longer fails (or bundles IDE junk) when packing the template on a developer machine. Scaffolded output now builds and self-hosts cleanly. - Scaffolded apps log in out of the box, get isolated data volumes, and start on
main— threefsh new/ Aspire DX fixes: the AppHost migrator now runsapply --seed, so the root admin (admin@root.com) is seeded automatically — previously a freshly-run app came up with an empty user table and nobody could log in; each app’s Docker volumes are namespaced by app name (e.g.myapp-postgres-data) instead of sharing a literalpostgres-data, so two FSH-based apps on one machine no longer clobber each other’s database; andfsh newinitializes git onmainrather than following the machine’s git default (oftenmaster). - Demo logins (
acme/globex) work on a fresh Aspire launch — the dashboard’s demo-login panel advertised accounts that were never seeded: the AppHost migrator ran onlyapply --seed(which seeds the root admin), while theacme/globexdemo tenants are created by the dev-onlyseed-demoverb. Aspire now runsseed-demoas a dedicated demo-seeder step after migration — soadmin@acme.com/Password123!works the moment the dashboard loads. Also fixes the migrator crashing at startup in Development (its trimmed service graph tripped the DI container’s build-time validation) and corrects the verb’s environment gate toDOTNET_ENVIRONMENT(the migrator is a generic-host console app, not a web host). - Aspire resource names are namespaced per app — the AppHost’s resource/container names (API, migrator, demo-seeder, admin, dashboard) now derive from the app’s namespace, like the Docker volume names already did. A scaffolded
Acme.Storeshowsacme-store-apietc. instead of the kit’s literalfsh-*, so two FSH-based apps on one machine don’t collide. (This repo resolves tofsh-starter-*; thepostgres/redis/minioinfra and thefsh-dbdatabase keep stable names.) - Stale sessions resolve cleanly instead of erroring — both React apps (admin + dashboard) treated an expired token left in
localStorageas signed-in, firing protected requests that 401’d in a loop (SecurityTokenExpiredException). On boot they now attempt one silent token refresh: success restores the session, failure routes to/login. Long-lived sessions still refresh transparently mid-use. - CI split into path-scoped backend + frontend pipelines — the single
ci.ymlis replaced bybackend.yml(runs only onsrc/**changes) andfrontend.yml(runs only onclients/**), so a client-only change never builds or tests the API, and vice versa. The SDK is pinned to the .NET 10 GA release via a rootglobal.json(no more preview channel). Unit and integration tests each run once, and the coverage gate merges their results instead of re-running the whole solution. The React apps get real CI for the first time — ESLint,tsc/Vite build, and the Playwright E2E suites (admin + dashboard) on Node 22. Branch protection requires the always-resolvingBackend CI/Frontend CIgate jobs. See CI/CD. - Consolidated to a single
mainbranch — the repo now uses one long-lived default branch,main; thedevelopbranch is retired. Branch from and targetmain; stable releases are cut fromv*tags. See Contributing. - Removed the redundant root
docker-compose.yml— local development is covered by .NET Aspire and production bydeploy/docker/, so the overlapping root compose file (added 2026-05-24) was dropped. - Missing required request parameters now return
400, not500— calling a tenant-scoped endpoint without thetenantheader (and any other endpoint missing a required header/route/query parameter, or sent with an unreadable/oversized body) raised an ASP.NETBadHttpRequestExceptionthat the global exception handler rendered as a generic500 Internal Server Error. The handler now honours the framework’s own status code, so these surface as a proper400 Bad Request(or413, etc.) with aProblemDetailsbody. Fixes #1245.
2026-05-24
- Cache/store engine switched from Redis to Valkey 8 — the BSD-licensed, Linux Foundation fork of Redis. It’s a drop-in over the Redis protocol (RESP): the
StackExchange.Redisclient and everyCachingOptions:Redisconfig key are unchanged. Applies to .NET Aspire, both Docker Compose files, and the integration-test container. - RedisInsight cache browser is now auto-wired in Aspire, connected to the Valkey instance so you can inspect cache keys, TTLs, and the SignalR backplane in local dev with no manual configuration.
- Docker Compose hardening — the production
deploy/dockerstack now provisions the MinIO bucket before the API starts (fixes a first-uploadNoSuchBucket); the dev rootdocker-compose.ymlnow runs the DB migrator (apply --seed) so the API never boots against an empty schema.