Skip to content
fullstackhero

Reference

Overview

Release notes and version history for fullstackhero.

views 0 Last updated

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 the ChannelMember rows; 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 got 404 trying 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 Closed state was unreachable (no way to get there) and the Tickets.Update / Tickets.Delete permissions 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 new Tickets.Close permission. GET /api/v1/tickets/{id}/comments now returns 404 for 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 new Webhooks.View / Create / Delete / Test permissions. After upgrading, grant these to the roles that manage webhooks or those users will get 403. See Webhooks.
  • Multitenancy & correctness. The GET tenant provisioning status and retry provisioning endpoints now accept and forward a CancellationToken (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 (a pageSize=0 previously surfaced as a 500, now a clean 400); and the Billing monthly-invoice job takes its clock from TimeProvider for deterministic tests.
  • Known follow-up. Billing publishes its InvoiceIssued event directly on the in-memory bus rather than through the transactional outbox (Files does the same, with no consumer yet). Closing this needs a small Eventing building-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 fsh CLI gained a --version flag and a corrected (semver-aware) update check; the unimplemented --db sqlserver scaffold 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 swappable IInvoicePdfRenderer); the download is tenant-scoped, so one endpoint safely serves both the operator console and tenant self-service. The dashboard gains a /subscription page (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 key Billing: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 behind IInvoicePdfRenderer if 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 a X-Subscription-Grace response 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-checked JoinChannel method 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 their user:{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 IsActive flag 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 with 403 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 packingdotnet new fsh / fsh new packed extensionless files (every Dockerfile) to a doubled nested path, so scaffolded projects got a Dockerfile directory instead of a file and deploy/docker (docker compose up) was broken. Also made the IDE-cache excludes (.vs/.idea/.vscode) recursive so dotnet pack no 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 — three fsh new / Aspire DX fixes: the AppHost migrator now runs apply --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 literal postgres-data, so two FSH-based apps on one machine no longer clobber each other’s database; and fsh new initializes git on main rather than following the machine’s git default (often master).
  • 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 only apply --seed (which seeds the root admin), while the acme/globex demo tenants are created by the dev-only seed-demo verb. Aspire now runs seed-demo as a dedicated demo-seeder step after migration — so admin@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 to DOTNET_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.Store shows acme-store-api etc. instead of the kit’s literal fsh-*, so two FSH-based apps on one machine don’t collide. (This repo resolves to fsh-starter-*; the postgres/redis/minio infra and the fsh-db database keep stable names.)
  • Stale sessions resolve cleanly instead of erroring — both React apps (admin + dashboard) treated an expired token left in localStorage as 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.yml is replaced by backend.yml (runs only on src/** changes) and frontend.yml (runs only on clients/**), 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 root global.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-resolving Backend CI / Frontend CI gate jobs. See CI/CD.
  • Consolidated to a single main branch — the repo now uses one long-lived default branch, main; the develop branch is retired. Branch from and target main; stable releases are cut from v* tags. See Contributing.
  • Removed the redundant root docker-compose.yml — local development is covered by .NET Aspire and production by deploy/docker/, so the overlapping root compose file (added 2026-05-24) was dropped.
  • Missing required request parameters now return 400, not 500 — calling a tenant-scoped endpoint without the tenant header (and any other endpoint missing a required header/route/query parameter, or sent with an unreadable/oversized body) raised an ASP.NET BadHttpRequestException that the global exception handler rendered as a generic 500 Internal Server Error. The handler now honours the framework’s own status code, so these surface as a proper 400 Bad Request (or 413, etc.) with a ProblemDetails body. 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.Redis client and every CachingOptions:Redis config 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/docker stack now provisions the MinIO bucket before the API starts (fixes a first-upload NoSuchBucket); the dev root docker-compose.yml now runs the DB migrator (apply --seed) so the API never boots against an empty schema.