Skip to content

FullStackHero 10 .NET Starter Kit Release Merge#1152

Draft
iammukeshm wants to merge 485 commits into
mainfrom
develop
Draft

FullStackHero 10 .NET Starter Kit Release Merge#1152
iammukeshm wants to merge 485 commits into
mainfrom
develop

Conversation

@iammukeshm
Copy link
Copy Markdown
Member

#Architecture

  • Modular monolith with modules for Identity, Multitenancy, Auditing; mediator-based CQRS; background jobs; caching; mailing; storage abstraction.
  • Minimal API host with Identity (JWT, refresh, roles/permissions), Multitenancy (Finbuckle, provisioning lifecycle), Auditing (request/response/security/exception with background sink).
  • Shadcn-inspired MudBlazor wrappers; Dashboard/Profile/Audits pages wired to generated API clients; BFF-style auth delegating handler; theme/layout shell.
  • NSwag config + script to regenerate clients (scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.
  • Multi-app AWS scaffolding (API/Blazor) with modular structure using Terraform.
  • Mediator Handlers and Validation
  • RateLimiting / Storage / Outbox Pattern

@iammukeshm iammukeshm requested a review from Copilot December 9, 2025 09:12
@iammukeshm iammukeshm self-assigned this Dec 9, 2025
@iammukeshm iammukeshm added the enhancement New feature or request label Dec 9, 2025
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the FullStackHero 10 .NET Starter Kit, implementing a modular monolith architecture with comprehensive modules for Identity, Multitenancy, and Auditing. The implementation includes a mediator-based CQRS pattern, JWT authentication with refresh tokens, role/permission-based authorization, background job support, caching abstractions, mailing services, and storage abstractions (local and S3). The Blazor client uses Shadcn-inspired MudBlazor wrappers with generated API clients via NSwag, while the infrastructure includes multi-app AWS scaffolding using Terraform and OpenTelemetry-based observability.

Key Changes

  • Modular architecture with separate Identity, Multitenancy, and Auditing modules implementing contracts and handlers
  • JWT authentication, role/permission system, and Finbuckle multitenancy with per-tenant provisioning lifecycle
  • Auditing pipeline with request/response/security/exception tracking and background sink for SQL persistence
  • OpenTelemetry integration, rate limiting, storage abstraction (local/S3), and comprehensive building blocks for caching, jobs, mailing, and persistence

Reviewed changes

Copilot reviewed 295 out of 1048 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Directory.Packages.props Updated package versions to .NET 10.0 and newer dependencies including Finbuckle 10.0.0, Mediator 3.1.0-preview.14, Hangfire 1.8.22, and OpenTelemetry 1.14.0
Directory.Build.props Enhanced with .NET 10.0 target, comprehensive code analysis settings, NuGet metadata, and stricter quality controls
BuildingBlocks/Web/*.cs New Web building block with OpenAPI/Scalar integration, OpenTelemetry, Serilog logging, rate limiting, security headers, CORS, versioning, and module loading
BuildingBlocks/Storage/*.cs Storage abstraction supporting local filesystem and AWS S3 with file type validation and upload/removal operations
BuildingBlocks/Shared/*.cs Shared contracts for multitenancy (AppTenantInfo), identity (claims, permissions, roles), pagination, and database options
BuildingBlocks/Persistence/*.cs Persistence infrastructure with specifications pattern, EF Core extensions, and database initialization interfaces
Modules/Identity/Modules.Identity.Contracts/*.cs Identity module contracts including commands/queries for token generation, user management, role management, and associated DTOs
Modules/Auditing/Modules.Auditing.Contracts/*.cs Auditing contracts with event types, payloads, DTOs, and interfaces for audit publishing, serialization, and sinking
Modules/Auditing/Modules.Auditing/*.cs Auditing implementation with SQL sink, EF interceptor, HTTP middleware for request/response capture, channel-based publisher, and query handlers

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@iammukeshm iammukeshm marked this pull request as draft December 9, 2025 09:13
@maxiar
Copy link
Copy Markdown
Contributor

maxiar commented Dec 10, 2025

#Architecture

  • Modular monolith with modules for Identity, Multitenancy, Auditing; mediator-based CQRS; background jobs; caching; mailing; storage abstraction.
  • Minimal API host with Identity (JWT, refresh, roles/permissions), Multitenancy (Finbuckle, provisioning lifecycle), Auditing (request/response/security/exception with background sink).
  • Shadcn-inspired MudBlazor wrappers; Dashboard/Profile/Audits pages wired to generated API clients; BFF-style auth delegating handler; theme/layout shell.
  • NSwag config + script to regenerate clients (scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.
  • Multi-app AWS scaffolding (API/Blazor) with modular structure using Terraform.
  • Mediator Handlers and Validation
  • RateLimiting / Storage / Outbox Pattern

Wow! You woke up!! :) Excelent works, I going to clone and test it... Please check, you forgot push the /docs folders because is added in .gitignore: "/docs"
BTW.... What IDE or stack you use or recommend to work better, get a good experience with this starter kit, may be VS 2026 + Copilot, or VS Code + Codex or Cursor + Another IA Model, what is your experience creating this template, Did you use any AI Assistance with some .MD spec files to define the software architect guidelines or something like that?

Thanks in advanced.

@iammukeshm
Copy link
Copy Markdown
Member Author

@maxiar

I am currently using VS2026.
Docs is ignored purposely, as they will be on another repo. It's still a WIP.
For AI Code Guidance, currently testing with Codex CLI. Trying to formulate a framework for a nice workflow experience. Will write about it on my blog once I figure it out.

@maxiar
Copy link
Copy Markdown
Contributor

maxiar commented Dec 12, 2025

@maxiar

I am currently using VS2026.

Docs is ignored purposely, as they will be on another repo. It's still a WIP.

For AI Code Guidance, currently testing with Codex CLI. Trying to formulate a framework for a nice workflow experience. Will write about it on my blog once I figure it out.

Perfect approach, check that may be some ideas are usefull:

https://medium.com/@mikhail.petrusheuski/steal-these-25-prompts-the-rules-workflows-that-made-our-net-team-faster-27899ece4dcc

And this "spec driven AI design":

https://medium.com/@mikhail.petrusheuski/steal-these-25-prompts-the-rules-workflows-that-made-our-net-team-faster-27899ece4dcc

@iammukeshm
Copy link
Copy Markdown
Member Author

iammukeshm commented Dec 12, 2025

@maxiar looks like its a member only story. any crucial takeaways?

@github-advanced-security
Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread src/BuildingBlocks/Web/Exceptions/GlobalExceptionHandler.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/ThemeStateFactory.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/ThemeStateFactory.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/ThemeStateFactory.cs Fixed
Comment thread src/Modules/Identity/Modules.Identity/Services/TokenService.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
Comment thread src/Modules/Identity/Modules.Identity/Services/TokenService.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
fullstackhero bb370a7 May 15 2026, 08:50 PM

Comment thread src/BuildingBlocks/Caching/DistributedCacheService.cs Fixed
Comment thread src/BuildingBlocks/Caching/DistributedCacheService.cs Fixed
Comment thread src/BuildingBlocks/Caching/HybridCacheService.cs Fixed
Comment thread src/BuildingBlocks/Caching/HybridCacheService.cs Fixed
Comment thread src/BuildingBlocks/Web/Idempotency/IdempotencyEndpointFilter.cs Fixed
Comment thread src/BuildingBlocks/Web/Idempotency/IdempotencyEndpointFilter.cs Fixed
iammukeshm and others added 7 commits March 27, 2026 03:11
New module: Modules.Webhooks with full CQRS vertical slice architecture:

Domain:
- WebhookSubscription: tenant-scoped, supports event filtering and wildcard (*)
- WebhookDelivery: tracks each delivery attempt with status and error

API Endpoints:
- POST   /api/v1/webhooks/subscriptions — create subscription
- DELETE /api/v1/webhooks/subscriptions/{id} — delete subscription
- GET    /api/v1/webhooks/subscriptions — list subscriptions (paged)
- GET    /api/v1/webhooks/subscriptions/{id}/deliveries — list deliveries
- POST   /api/v1/webhooks/subscriptions/{id}/test — send test event

Features:
- HMAC-SHA256 payload signing (X-Webhook-Signature header)
- Resilient HTTP delivery via AddHeroResilience (Polly v8)
- EF Core persistence with webhooks schema
- Health check integration
- FluentValidation on all commands
Add SSE BuildingBlock with:
- SseConnectionManager: manages per-user channels with tenant awareness
  - Connect/Disconnect lifecycle with bounded channels (100 events, drop oldest)
  - TrySend(userId): targeted push to specific user
  - Broadcast(tenantId): push to all users in a tenant
  - BroadcastAll(): push to all connected users
- SseEvent record: EventType, Data, optional Id for reconnection
- SSE streaming endpoint at GET /api/v1/sse/stream
  - text/event-stream content type with proper SSE formatting
  - Nginx proxy buffering disabled (X-Accel-Buffering: no)
  - Graceful disconnect on client cancellation
- EnableSse toggle in FshPlatformOptions
- MapSseEndpoints toggle in FshPipelineOptions

Next.js/React clients consume via native EventSource API.
- Footer: fix module links to /overview/, guides/adding-a-feature
- Content: fix /adding-a-feature/ → /guides/adding-a-feature/ in 3 pages
- Content: fix /identity/roles/ → /identity/roles-permissions/
- Content: fix /identity/sessions/ → /identity/sessions-groups/
- Content: fix /building-blocks/blazor-ui/ → /building-blocks/overview/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add MDX documentation for all Phase 2 features:
- building-blocks/http-resilience.mdx — Polly v8 resilience pipelines
- building-blocks/feature-flags.mdx — tenant-aware feature management
- building-blocks/idempotency.mdx — HTTP request deduplication
- building-blocks/sse.mdx — Server-Sent Events real-time push
- modules/webhooks/overview.mdx — webhook subscriptions and delivery

Update CLAUDE.md tech stack table with new features.
Flatten all nested documentation paths for better SEO:
- /modules/identity/roles-permissions/ → /roles-and-permissions/
- /modules/multitenancy/overview/ → /multitenancy/
- /building-blocks/caching/ → /caching/
- /cross-cutting/exception-handling/ → /exception-handling/
- /patterns/specifications/ → /specification-pattern/
- /deployment/aws-terraform/ → /aws-terraform-deployment/
- /guides/adding-a-feature/ → /adding-a-feature/

All URLs now: fullstackhero.net/dotnet-starter-kit/{keyword-slug}/
Max depth: 2 levels. Every slug is a search-friendly keyword.

Updated: sidebar config, all internal links, imports, nav, footer,
breadcrumbs, pagination, llms.txt, structured data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ersion updates

Add WAF module with AWS managed rule groups (CommonRuleSet, SQLi, IP Reputation,
Known Bad Inputs, Linux OS, rate limiting) and CloudWatch logging.

Add CloudWatch alarms module for ECS, RDS, Redis, and ALB monitoring with
SNS email notifications.

Add ECS auto-scaling with target tracking on CPU, memory, and request count.

Security hardening across all modules:
- VPC endpoint SG egress restricted to VPC CIDR (was 0.0.0.0/0)
- Flow logs IAM scoped to specific log group ARN (was Resource: *)
- Default VPC SG lockdown (deny all - AWS best practice)
- KMS encryption support for all CloudWatch log groups
- ALB desync mitigation mode and connection logging
- ECS container readonlyRootFilesystem and initProcessEnabled
- RDS IAM authentication and CloudWatch log exports
- Redis slow-log and engine-log delivery
- S3 SSL/TLS enforcement on all bucket policies by default

Version updates: PostgreSQL 17, Redis 7.2, AWS provider >= 5.90.0,
Graviton (t4g) instances for 20% better price-performance.

Clean up stale tfvars with plaintext passwords and obsolete variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… findings

HIGH — Log entries from user input (6 findings):
- Cache services: replace user-derived key in warning logs with key length
  (DistributedCacheService, HybridCacheService)
- Idempotency filter: hash the idempotency key (SHA256 short) before logging

MEDIUM — Private data exposure (3 findings):
- TokenService: log subject ID instead of masked email, remove MaskEmail()
- SimpleBffAuth: log tenant/subject instead of email hash, remove ComputeEmailHash()

No PII or user-controlled strings now flow to log sinks.
Comment thread src/Playground/Playground.Blazor/Services/SimpleBffAuth.cs Fixed
iammukeshm and others added 8 commits March 27, 2026 10:05
…oyment guide

Add Webhooks module to introduction, architecture, module-system, and
project-structure docs (module count, tables, file trees, code samples).
Rewrite aws-terraform-deployment from 163 to 933 lines covering all 9
reusable modules, 3-environment comparison, step-by-step deployment,
security architecture, cost optimization, and troubleshooting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llation hero

- Replace all em dashes with hyphens across 47 doc files
- Add scroll-reveal animations with staggered card entrances (IntersectionObserver)
- Add particle constellation background to hero section
- Add hero entrance animation and terminal typing effect
- Add smooth FAQ accordion using CSS grid-template-rows
- Add card hover lift micro-interactions (GPU composited)
- Add button hover polish and CTA glow pulse
- Respect prefers-reduced-motion throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump all mobile font sizes to readable range (0.88-1.05rem body text)
- Fix author image to fill container edge-to-edge
- Reduce hero bottom padding and stats section padding to close gap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rounded pill with green version badge, hover glow, and arrow.
Links to quick-start. Collapses to version-only on small screens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Max-width 52rem, image column narrowed to 280px.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e positioning

Grid uses 340px fixed column instead of auto. Image uses
position absolute with all properties !important to override
Astro's generated inline styles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iammukeshm and others added 30 commits May 13, 2026 17:17
Consecutive own-author messages were stacking too tight (pt-0.5 + pb-1
= 6px gap) and on short bubbles ("hi" / "?") read as overlap. Bumped
the per-row vertical rhythm to py-1.5 with pt-1 on merged rows
(non-merged pt-3), so consecutive bubbles now sit ~10px apart while
still grouping into the same author block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- Message aggregate carries IsPinned + PinnedByUserId + PinnedAtUtc with
  domain-enforced rules: Pin() rejects deleted messages, no-ops if already
  pinned by the same user; Unpin() is idempotent. New MessagePinned /
  MessageUnpinned domain events.
- Three feature folders mirror the existing Edit/Delete pattern:
  PinMessage (POST /messages/{id}/pin), UnpinMessage (DELETE), and
  GetPinnedMessages (GET /channels/{id}/pinned — most-recent first).
- AddMessagePinning migration adds the three columns + a partial index
  on (ChannelId, IsPinned) WHERE IsPinned = true to keep the pinned-list
  query cheap.
- Handlers verify channel membership and broadcast ChatMessagePinned /
  ChatMessageUnpinned (full MessageDto) to channel:{id}.
- MessageDto + ChatMappers extended; permission Messages.Send is reused
  for the gate.

Frontend:
- Hover action rail gets a Pin / Unpin glyph between Reply and Edit.
- Pinned messages render with a small "Pinned" mono caption above the
  bubble and a brand-tinted ring on the bubble itself.
- New ChatPinnedDropdown in the channel header — Pin icon button opens
  a popover listing pinned messages; click jumps the feed via
  MessageListHandle.jumpToMessage.
- Realtime handlers patch top-level + per-parent caches and invalidate
  the pinned-panel cache on Pinned / Unpinned events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (BuildingBlocks/Web/Realtime):
- New IPresenceTracker singleton — concurrent dictionary keyed by userId
  tracking open connection counts. Returns true on 0→1 and 1→0 so the hub
  can broadcast exactly one transition.
- AppHub.OnConnectedAsync increments and broadcasts PresenceChanged
  { userId, online: true } to Clients.All on the first connection; the
  new OnDisconnectedAsync decrements and broadcasts online: false on the
  last close.
- New GET /api/v1/realtime/presence?userIds=a,b,c endpoint returns the
  snapshot for any set of ids; used by clients to bootstrap on mount
  before the SignalR PresenceChanged stream fills in.
- Registration wired through AddHeroRealtime / MapHeroRealtime so any
  app using the BuildingBlocks gets presence for free.

Note: in-memory tracker is per-host. Multi-replica deployments need a
shared store (Redis pub/sub on connect/disconnect); that's deferred.

Frontend:
- New usePresence(userId) hook in src/realtime/use-presence.ts —
  useQuery against the snapshot endpoint with refetchInterval 60s plus
  a PresenceChanged hub subscription that flips the cached value
  instantly. TanStack dedupes per-userId, so N messages from 5 unique
  authors fire 5 queries, not N.
- ChannelRail's 1-on-1 DM avatars now show an online/offline dot driven
  by usePresence.
- Message bubble avatars (other-side branch only — own avatars are
  hidden in the Teams layout) also show the dot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ arch updates

PinMessageTests (10):
- Pin / Unpin happy path with stamp verification
- Re-pin idempotence by the same user (no stamp change)
- Unpin no-op when not pinned
- GetPinnedMessages returns most-recently-pinned-first
- Empty list for unpinned channels
- 404 for missing message + non-member (don't leak channel existence)
- ChatMessagePinned / Unpinned broadcasts arrive on the channel group

PresenceTests (8):
- Snapshot endpoint reports offline for users without a hub connection
- Online while a hub is connected
- Drops back to offline on disconnect
- Multiple userIds in one call
- PresenceChanged fires on connect and on last-connection close
- Endpoint requires authentication

Backend additions to make the tests pass cleanly:
- PinMessageCommandValidator + UnpinMessageCommandValidator so the
  arch test HandlerValidatorPairing accepts the new commands.
- EndpointConventionTests: added Pin/Unpin to the action-verb whitelist.

Full chat suite now: 80 integration + 36 unit + 48 architecture = 164
tests, all passing (2 pre-existing skips on idempotency).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… group

Two correctness fixes flagged in yesterday's ship-readiness review.

Live read receipts:
MarkChannelReadCommandHandler now ALSO broadcasts ChatChannelMemberRead
to the channel:{id} group (not just user:{caller}), so other members
observe the watermark update without waiting for a manual refresh.
Payload: { channelId, userId, lastReadMessageId }. MessageList in the
dashboard patches the cached channel.members on receipt — ReadReceipt
re-derives instantly.

Presence scope:
AppHub now joins a tenant:{tenantId} group on connect (reading the
tenant claim from the principal) and broadcasts PresenceChanged to
that group instead of Clients.All. A 1000-user tenant no longer
fans every connect / disconnect out to every other tenant. Falls
back to Clients.All only when the tenant claim is missing.

No frontend change for presence — usePresence already subscribes to
PresenceChanged regardless of where the broadcast came from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…migrations

A new one-shot console project under src/Host/FSH.Starter.DbMigrator
that brings every database in the solution up to head and optionally
seeds, then exits with code 0 on success or 1 on failure. Replaces the
"migrate at API startup" pattern for production deployments; the API's
auto-migrate stays enabled in Development only.

Usage:
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --tenant root
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --catalog-only
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- seed
  dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending

How it works:
1. Builds the same DI container the API does (every module's
   ConfigureServices), with web-only concerns (CORS, OpenAPI, jobs,
   mailing, SSE, realtime, OpenTelemetry, quotas, idempotency) off.
2. RemoveAll<IHostedService> so background workers don't interfere.
3. Migrates TenantDbContext and seeds the root tenant if missing
   (same logic TenantStoreInitializerHostedService runs at API
   startup — extracted here so the migrator can be the single source).
4. Iterates every AppTenantInfo and calls
   ITenantService.MigrateTenantAsync (and SeedTenantAsync when --seed
   is set), reusing the production code path so behaviour stays
   identical to the API's auto-migrate.

Configuration shares appsettings.json + appsettings.Development.json
with FSH.Starter.Api via linked file references — operators override
with environment variables in production. Ships a DefaultContainer
publish profile (image name: fsh-db-migrator) so it can run as a
Kubernetes Job / Helm pre-install hook / CI step. README walks through
deployment patterns.

MigratorCommand.cs is a minimal arg parser (no System.CommandLine
dependency for half a dozen flags).

Slnx updated to include the new project under /Host/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e stutter

SendMessageCommandHandler broadcasts ChatMessageCreated via SignalR
before the HTTP response flushes, so the echo usually beats the
composer's onSuccess. The cache would briefly hold BOTH the optimistic
temp and the real DTO — a visible duplicate bubble.

ChatMessageCreated handler now locates the own pending temp by
(authorUserId, body, parentMessageId) — the only fields we control on
the client without a clientId round-trip — and removes it before
inserting/appending the real payload. Same dedup applies to the
top-level prepend path and the reply append path. The composer's
onSuccess cleanup remains as the fallback when the HTTP response wins
the race instead.
Three targeted improvements on top of the initial migrator.

Aspire orchestration:
- AppHost.csproj references the migrator project.
- AppHost.cs adds the migrator as an Aspire resource that WaitFor(postgres)
  and runs once with verb "apply"; the API now WaitForCompletion(migrator)
  so it never starts against an unmigrated database. Dev parity with
  production deploys: the migrator is the single source of migration truth.
- appsettings.Development.json flips MultitenancyOptions.RunTenantMigrations
  OnStartup to false so the API no longer double-migrates in dev. The
  migrator runs first; the API expects an up-to-date schema.

Postgres advisory lock (concurrent-run safety):
- New PostgresMigratorLock with a dedicated session-level pg_advisory_lock
  on a constant 64-bit key (0xFE514EC0_DEB1ADE4). Concurrent invocations
  block at acquire; whichever wins runs alone. Lock auto-releases on
  connection close, so a crashed migrator doesn't orphan it.
- First-run safety: if the target database doesn't exist yet (SQLSTATE
  3D000), the lock acquire falls back to a no-op holder — EF will create
  the database on MigrateAsync, and subsequent runs get the real lock.

Wait-for-database with backoff:
- WaitForDatabaseAsync polls Postgres with 1s → 10s exponential backoff
  up to a 2-minute deadline. Catches Aspire / K8s cold-starts where the
  Postgres container is still initialising when the migrator boots.
- Treats SQLSTATE 3D000 as a success (server reachable, target DB
  missing — EF creates it on Migrate). Treats transient Npgsql / socket
  / timeout exceptions as retryable.

What's still on the to-do list (not in this commit): --script mode for
DBA SQL review, parallel per-tenant for thousand-tenant scale, JSON
output for CI/CD parsing. None are blocking for current scale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dService surgery

Three corrections needed for the migrator to actually run end-to-end:

1. NoOpJobService satisfies IJobService in the DI graph.
   Multitenancy's TenantProvisioningService constructor takes IJobService,
   which Hangfire registers only when EnableJobs is true. We can't enable
   Hangfire in the migrator (it needs its own DB schema + worker threads),
   so a throwing no-op satisfies the constructor chain. All operations
   throw with a clear error message in the unlikely event a future
   migration path tries to enqueue something.

2. RemoveAll<IHostedService> was too aggressive — it stripped Serilog's
   flush-on-shutdown service so logs never reached stdout. Now we
   surgically remove only TenantStoreInitializerHostedService (the one
   that would race with our explicit catalog migration).

3. Operator-facing progress messages switched from ILogger.LogInformation
   to Console.WriteLine so they're guaranteed to surface regardless of
   Serilog config quirks. Internal logging via ILogger still flows for
   diagnostic info; the visible "migrating…/done/finished" lines are
   reliable.

Verified end-to-end against the Aspire-managed Postgres:

  [migrator] waiting for postgres…
  [migrator] postgres ready
  [migrator] acquiring advisory lock…
  [migrator] advisory lock acquired
  [tenant-catalog] already at head
  [root] migrating…
  [acme] migrating…
  [globex] migrating…
  [migrator] finished successfully.

Exit code 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier bump of merged-row pt to pt-1 (10px gap with py-1.5's pb) still
read as overlap on long sequences of short bubbles (a string of "?" /
"hi" / "ok" messages). The bubble rounding + brand-tinted background
visually fused them into one shape.

Bumped baseline to pb-1 only (4px below), pt-2 on merged (8px above —
total 12px between merged-merged), pt-4 on non-merged (16px above —
total 20px between author-changes). Long runs of short messages now
clearly separate while staying grouped as one block.
Two related visual issues addressed together.

Halo:
The bubble had transition-shadow duration-500 layered on top of three
possible box-shadow states (default 1px hairline, isFlashing 3px ring,
isPinned 1px primary ring via Tailwind's ring-* utility). CSS shadow
interpolation between those states during state transitions produced a
ghost halo that bled well outside the rounded fill — most visible on
strings of short merged bubbles where two halos overlapped.

Fixed by dropping transition-shadow and the layered hairline shadow.
The flash is now a sharp 2px ring that snaps on and off; pinned stays
a 1px static ring. No shadow interpolation means no smearing.

Spacing:
Even with the halo gone, merged bubbles still read as too tight on the
"?-?-?-?" pattern. Bumped baseline pb to pb-1.5 (6px) and merged-row
pt to pt-3.5 (14px) — total ~20px between merged rows. Non-merged
author-change rows use pt-6 (24px) for a clear ~30px break.
…adding

Root cause of the "weird, non-uniform" spacing was the row's outer
padding flipping between two values: pt-3.5 when isMerged, pt-6 when
not. Because the author header lives INSIDE the inner column (above
the bubble) only on non-merged rows, the bubble's position relative to
the row's top kept shifting:

  merged row:      [pt-3.5][bubble][pb-1.5]
  non-merged row:  [pt-6][header+mb][bubble][pb-1.5]

That meant the gap between successive BUBBLES varied depending on which
side of the boundary you were on (merged→merged, merged→non-merged,
non-merged→merged each produced a different bubble-to-bubble distance).

Fix: every row now uses the same pt-2 / pb-1 — 12px between any two
adjacent bubbles, period. The author header on non-merged rows still
provides its own visual block-break by occupying space ABOVE the bubble
inside the inner column, with a consistent mb-1 (4px) gap to the bubble
below it. Two consecutive merged bubbles look exactly the same as a
non-merged bubble followed by a merged one — what changes is what
sits between them inside the row, not the row-to-row offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esn't reject

Chat attachments are uploaded with Visibility=Private so the
ChatChannelFileAccessPolicy can gate reads to channel members. That
means FileAssetDto.publicUrl is null, and the composer was passing an
empty string as the message attachment Url — which the server's
MessageAttachment.Create rejects with ArgumentException, surfacing as
"SEND FAILED — RETRY?" on the composer.

After a successful upload, immediately call getFileDownloadUrl(id,
{inline: true}) when publicUrl is empty to mint a presigned read URL.
The resulting non-empty Url satisfies the server's validation AND lets
the recipient render the attachment without an extra round-trip.

Known limitation: the presigned URL expires (typically 1 hour). Old
chat attachments would 403 on re-render after the window passes. Proper
fix is a backend change — make MessageAttachment.Url nullable and
resolve via fileAssetId at read time — left as follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…istic temps

Two related bugs surfaced by attempting to send a file with no
accompanying text.

Send 400 "'Body' must not be empty":
- SendMessageCommandValidator unconditionally required Body. Now it
  requires "Body OR at least one attachment" — Slack/Teams parity.
- SendMessageCommand.Body and SendMessageBody.Body switched to string?
  so empty/missing bodies round-trip cleanly.
- Message.Create accepts null/empty body and stores null (column was
  already nullable in EF config). Aggregate stays consistent because
  the validator enforces the "must have content" invariant on input.
- SendMessageCommandHandler runs MentionParser against cmd.Body ??
  string.Empty so an attachment-only send doesn't NRE on parse.

MarkChannelRead 500:
- The optimistic-send temp message has id "temp:xxx". Its momentary
  presence at the head of the messages cache made chat-page's mark-read
  effect POST that string to the server, which can't parse it as a
  Guid and 500s. Skip when latestMessageId.startsWith("temp:") —
  the real id arrives via realtime echo and the effect re-fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 new tests covering everything under Modules/Catalog/.../Features/v1/
Products/ that wasn't previously tested. Mirror of BrandsEndpointTests
so the two halves of the catalog API have parity.

Happy path:
- CreateProduct returns 200 + persists all fields
- SearchProducts includes a newly created product
- SearchProducts filters by brandId
- UpdateProduct persists changes + flips IsActive + bumps UpdatedAtUtc
- ChangeProductPrice persists the new amount + currency
- AdjustProductStock applies positive deltas
- AdjustProductStock applies negative deltas in-range

Soft-delete + restore:
- DeleteProduct hides from search, returns 404 on GetById, appears in
  /products/trash with DeletedOnUtc + DeletedBy stamped
- RestoreProduct brings the row back to GetById visibility, clears
  audit stamps
- DeleteProduct → CreateWithSameSku succeeds (filtered unique index)

Business rules:
- AdjustProductStock rejects a negative delta that would push stock < 0
- CreateProduct returns 409 on duplicate live SKU
- CreateProduct returns 400 on empty SKU
- CreateProduct returns 400 on negative price
- CreateProduct returns 400/404 on unknown brand
- GetProductById returns 404 for unknown id
- DeleteProduct returns 404 for unknown id
- RestoreProduct returns 404 for unknown id

Auth gating:
- 401 on Create / Search / Delete / ListTrashedProducts without auth

Idempotency:
- CreateProduct without an Idempotency-Key header never gets the
  Idempotency-Replayed response header

Uses FSH.Modules.Catalog.Contracts.Dtos.ProductDto directly (same as
ProductImagesTests). PickBrandAndCategoryAsync picks the first seeded
brand + category — keeps tests independent of seed data identity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s intact

The new ProductsEndpointTests caught a real bug. DeleteProduct was
returning 500 with:

  null value in column "PriceAmount" of relation "Products" violates
  not-null constraint

Root cause: when EF marks an aggregate for deletion, every owned-type
navigation cascades to EntityState.Deleted. The AuditableEntity
interceptor flips the parent back to Modified to perform the soft
delete, but the owned references stay marked for deletion — so the
generated UPDATE includes "SET PriceAmount = NULL, PriceCurrency =
NULL", which violates the NOT NULL columns.

Brand soft-delete works because Brand has no owned types. The
regression is specific to aggregates with owned value objects
(Product → Money Price; future aggregates will hit the same).

Fix: in the same interceptor, after restoring the parent's state,
walk every owned reference whose TargetEntry is also Deleted and
restore it to Unchanged. The owned values then survive the UPDATE
unchanged, only IsDeleted / DeletedOnUtc / DeletedBy mutate.

Also fixed in ProductsEndpointTests:
- BrandDto fully qualified to Infrastructure.BrandDto (catalog DTO
  was ambiguous after referencing the contracts assembly directly).
- UniqueSku helper upper-cases the prefix portion since Product.Create
  canonicalises the SKU to upper.

Verified: 23/23 new product tests pass; full integration suite 282/285
(3 pre-existing skips, 0 failures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI builds with -warnaserror; the new DbMigrator project tripped CA1303,
CA1308, CA1515, CA1812, CA1849, CA1873, CA2100, CA1031, S1186, S6618,
S6667. Local builds didn't surface these because we don't pass the
flag — fixing per-file rather than relaxing the analyzer set so the
gap stays caught.

- DbMigrator.csproj: NoWarn CA1303 (operator-facing console; not localised)
- MigratorCommand: internal sealed record; OrdinalIgnoreCase verb match
- PostgresMigratorLock: LoggerMessage source-gen, parameterised
  pg_advisory_lock/unlock SQL, CultureInfo.InvariantCulture for the
  TimeoutException message
- Program: async Console writers, string.Create for interpolation,
  scoped CA1031 suppression around Main's broad catch
- NoOpJobService: SuppressMessage CA1812 (DI-activated)
- ProductImageValueGenerationNever migration: documented intentionally
  empty Up/Down

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local builds previously stayed green even with analyzer warnings, so
regressions surfaced only when CI ran `dotnet build … -warnaserror`.
Flipping TreatWarningsAsErrors + CodeAnalysisTreatWarningsAsErrors to
true in Directory.Build.props closes the gap — `dotnet build` (no flag)
now fails on the same warnings CI does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…only path

Strips both startup-time migration hosted services and the two
MultitenancyOptions flags that gated them. The API no longer applies
schema changes on boot — FSH.Starter.DbMigrator (already production-grade,
chained by Aspire AppHost) is the single deploy-time migration path.
On-demand tenant provisioning via POST /tenants is preserved.

Production-readiness teeth: TenantMigrationsHealthCheck now returns
Unhealthy when any tenant has pending migrations, so /health/ready
returns 503 and Kubernetes readiness probes pull the pod out of rotation
until DbMigrator catches the schema up.

DbMigrator hardening: fail-fast on empty connection string before host
build (clean message, exit 1) and log current_user/current_database on
connect so a misconfigured low-priv connection string is caught at boot
instead of at the first DDL.

CI: new migrator-smoke job publishes the migrator container locally and
runs apply --catalog-only against an ephemeral Postgres — catches
container-publish regressions before deploy.

Regression: TenantMigrationsHealthCheckTests asserts the Unhealthy →
Healthy transition against a real Postgres testcontainer plus the
Unhealthy-on-probe-throw path. Implemented as a direct unit test (not
through WebApplicationFactory) to avoid the Hangfire static state that
would otherwise leak across the shared collection fixture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Message.Create` was relaxed in 9edaafe to accept null/empty/whitespace
body so attachment-only messages can pass through the aggregate; the
"must have content" invariant moved to SendMessageCommandValidator.

The Create_Should_Reject_Empty_Body test still asserted the old throw
behavior and has been failing since that commit (the 2026-05-14 audit
ran only 6 of 8 suites and missed it). Rewritten as
Create_Should_Normalize_Whitespace_Body_To_Null to lock in the current
contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 4.11 API

Patch bumps across the .NET 10 stack:
- ASP.NET Core / EF Core / Identity / Caching / Configuration / DI /
  Hosting / Logging / Options / OpenAPI / SignalR.Client+Redis /
  Mvc.Testing: 10.0.7 -> 10.0.8
- Http.Resilience / ServiceDiscovery / Caching.Hybrid: 10.5.0 -> 10.6.0
- Finbuckle.MultiTenant.*: 10.0.7 -> 10.0.8
- Scalar.AspNetCore: 2.14.7 -> 2.14.11
- AWSSDK.S3: 4.0.22.1 -> 4.0.23.1
- Spectre.Console: 0.55.0 -> 0.55.2
- Testcontainers.{PostgreSql,Redis,Minio}: 4.5.0 -> 4.11.0

Testcontainers 4.11 removes the `new XBuilder("image:tag")` constructor
overload; switched all three integration-test container builders to the
fluent `.WithImage("image:tag")` form.

Build green (0 warn / 0 err under TreatWarningsAsErrors). 598 unit tests
across 8 suites pass. Integration tests not exercised (Docker-gated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn MCP + dotnet build + dotnet test audit across 45 projects / 42,314 LoC.
0 errors, 0 warnings, 0 circular dependencies, 539 unit tests green (the 6
suites the audit ran). All 768 raw anti-pattern findings and 21 dead-code
candidates triaged: ~10 real design-choice items, the rest false positives
or generated-code noise. Five optional recommendations, none blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING: how to report bugs / vulns / feature requests, dev setup,
PR workflow (target develop, Conventional Commits, no BuildingBlocks
edits without discussion), pointer to CLAUDE.md for architecture rules.

SECURITY: starter-kit support model (develop only — forks own their
own fixes), private reporting via GitHub Security Advisories, 72h ack
/ 7d triage / ~90d disclosure window, scope, production hardening note.

CODE_OF_CONDUCT.md intentionally deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps transitive (vite, defu, postcss) and direct (astro, @astrojs/cloudflare)
packages within existing caret ranges. Addresses high-severity vite path
traversal, fs.deny bypass, WebSocket file read, and defu prototype pollution,
plus moderate XSS in astro/postcss and low-severity SSRF in @astrojs/cloudflare.
package.json unchanged; build verified (52 pages indexed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cs link

- Catalog: 23 CategoriesEndpointTests covering CRUD, parent/child
  hierarchy with cycle detection, tree retrieval, soft-delete +
  restore, business rules, auth gating, and idempotency.
- Notifications: 19 NotificationsEndpointTests covering the four
  inbox endpoints — auth gating, empty inbox, single-row lifecycle
  (incl. cross-user MarkRead safety and idempotent re-mark), bulk
  read, unreadOnly filter, newest-first ordering, paging.
- Billing: 28 BillingEndpointTests covering Plans CRUD,
  Subscriptions assign/replace/lookup, the Invoice state machine
  (Draft → Issued → Paid/Void with rejection guards), and the
  Generate flow with per-period idempotency.
- README: fix broken docs link to the new Astro Starlight site at
  docs/src/content/docs/ (resolves #1205).
- docs/ISSUE_TRIAGE_2026-05-14.md: triage record for all 23 open
  issues — 18 closed with tailored comments, 5 kept open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch from .WithImage("...") to constructor-with-image overload to
match commit bdfee3b — these three files were missed when the rest
of the test infra was migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DevDataSeeder: source SharedPassword from Seed:DemoPassword config
  instead of a C# literal; resolve lazily inside ExecuteAsync so missing
  config doesn't crash host startup. Adds Seed:DemoPassword=Password123!
  to appsettings.Development.json and the integration test factory's
  in-memory config (preserves existing seed behavior).
- ConfigureJwtBearerOptions: add SanitizeForLog helper to strip control
  characters from Request.Method/Path before logging, defending against
  log-injection via attacker-controlled request data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK 10.0.300 errors CONTAINER2008 when both ContainerImageTag and
ContainerImageTags are set. src/Directory.Build.props seeds the plural
form globally (10.0.0-rc;latest), so the smoke step's singular -p
override collides. Switch to the plural form to match the prod/dev
publish steps and let -p override the default cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ainer target

The previous CONTAINER2008 fix (singular -> plural ContainerImageTag(s) on the
smoke step) wasn't enough — the real source of the collision is the obsolete
PublishProfile=DefaultContainer pattern. In SDK 10.0.300 it auto-derives
ContainerImageTag (singular) from \$(Version) = 10.0.0-rc.1, which then collides
with ContainerImageTags from Directory.Build.props. Hence the NETSDK1198 warning
("DefaultContainer profile not found") immediately followed by CONTAINER2008.

Switch to the modern /t:PublishContainer target everywhere (csprojs no longer
need PublishProfile, the API publish CI steps drop -p:PublishProfile=DefaultContainer).
Verified locally with -p:ContainerArchiveOutputPath — both DbMigrator (single
'smoke' tag) and API (two tags) publish cleanly, no warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants