{"owner":"fullstackhero","repo":"dotnet-starter-kit","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","AGENTS.md","GEMINI.md",".agents/skills/add-entity/SKILL.md",".agents/skills/add-feature/SKILL.md"],"skills":{"CLAUDE.md":"# Claude Code\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n","AGENTS.md":"# FullStackHero .NET Starter Kit\n\n> A production-ready modular .NET 10 monolith + two React 19 apps, built for enterprise SaaS.\n\nThis file is the canonical guide for **all** AI coding tools (Claude Code, Gemini CLI, Cursor, Codex, …).\n`CLAUDE.md` and `GEMINI.md` are thin bridges that import this file — edit conventions **here**, not there.\n\nThis file is the map. Detailed conventions live in `.agents/rules/` and are read on demand — **read the\nrelevant rule file before working in that area** (see the index below). Keep this file lean.\n\n## What this is\n\nA **modular monolith** (Vertical Slice Architecture) backend that ships with two **React + Vite**\nfront-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and more are first-class.\n\n- **Backend** — .NET 10, EF Core 10, PostgreSQL, Redis, JWT + ASP.NET Identity, Finbuckle multitenancy,\n  Hangfire, OpenAPI/Scalar, Serilog + OpenTelemetry, .NET Aspire.\n- **Frontends** — `clients/admin` (operator-facing) and `clients/dashboard` (tenant-facing): React 19,\n  Vite 7, TypeScript, TanStack Query v5, React Router 7, Radix + Tailwind v4 (shadcn-style), SignalR/SSE.\n\n## Repo map\n\n| Path | What |\n|------|------|\n| `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…). **Protected — see below.** |\n| `src/Modules/{Name}/` | Bounded contexts. Each has a runtime project + a `.Contracts` project (its only public API). |\n| `src/Host/FSH.Starter.Api` | Composition-root Web API host. |\n| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Redis, MinIO, migrator, API, **both React apps**). |\n| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner. DB is **not** migrated at API startup. |\n| `src/Host/FSH.Starter.Migrations.PostgreSQL` | All EF migrations, organized per-module by folder. |\n| `src/Tests/` | Per-module tests, `Architecture.Tests` (NetArchTest), `Integration.Tests` (Testcontainers). |\n| `src/Tools/CLI` | The `fsh` CLI (Spectre.Console). |\n| `clients/admin`, `clients/dashboard` | The two React apps. |\n| `deploy/` | Infra (docker, terraform, dokploy). |\n\n## Tech stack\n\n| Backend | | Frontend | |\n|---|---|---|---|\n| Framework | .NET 10 / C# latest | Framework | React 19 + Vite 7 + TS 5.x |\n| CQRS | Mediator 3.x (source-gen) | Data | TanStack Query v5 |\n| Validation | FluentValidation 12.x | Routing | React Router 7 |\n| ORM / DB | EF Core 10 / PostgreSQL (Npgsql) | UI | Radix + Tailwind v4 + CVA (shadcn) |\n| Auth | JWT Bearer + ASP.NET Identity | Forms | react-hook-form + zod (**admin only**) |\n| Multitenancy | Finbuckle 10.x | Realtime | `@microsoft/signalr`, SSE (dashboard) |\n| Cache / Jobs | Redis, Hangfire | Tests | Playwright (route-mocked) |\n| Docs | OpenAPI + Scalar | API client | hand-written `apiFetch` (no codegen) |\n| Hosting | .NET Aspire | Env | runtime `/config.json` (not `VITE_*`) |\n| Testing | xUnit, Shouldly, NSubstitute, AutoFixture, NetArchTest, Testcontainers | | |\n\n## Build & run\n\n```bash\n# Whole stack (Postgres + pgAdmin + Redis + MinIO + migrator + API + both React apps)\ndotnet run --project src/Host/FSH.Starter.AppHost   # one-time: npm install in clients/admin & clients/dashboard\n\ndotnet build src/FSH.Starter.slnx                   # build backend\ndotnet run --project src/Host/FSH.Starter.Api       # API only → https://localhost:7030 (/scalar)\ndotnet test src/FSH.Starter.slnx                    # tests — integration tests REQUIRE Docker\n\ncd clients/admin && npm install && npm run dev       # → http://localhost:5173\ncd clients/dashboard && npm install && npm run dev   # → http://localhost:5174\n```\n\nMigrations / seed (DbMigrator, separate step):\n```bash\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- apply [--seed]\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending\n```\n\n**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · MinIO 9000/9001.\n\n## Branching & PRs\n\nSingle long-lived branch: **`main`** (the default) — there is **no `develop`**. Branch from and target `main`; stable releases are cut from `v*` tags. CI is split into path-scoped **Backend CI** (`src/**`) and **Frontend CI** (`clients/**`) workflows; branch protection requires only those two gate checks — never the individual jobs, which are skipped on the other side's PRs.\n\n## Golden rules (do not break)\n\n1. **Module boundaries** — a module references another module only through its `.Contracts` project, never its runtime project. Enforced by `Architecture.Tests`.\n2. **Registering a module touches FOUR places** — `Program.cs` Mediator `o.Assemblies` (two markers each) + `moduleAssemblies` array, **and the identical pair in `DbMigrator/Program.cs`**. A missing Mediator marker = handlers silently undiscovered. See `architecture.md`.\n3. **Tenant isolation is default-ON** via `BaseDbContext`. Opt out only via `IGlobalEntity`. Subclass DbContexts call `base.OnModelCreating` **last**. See `database.md`.\n4. **Do NOT modify `src/BuildingBlocks`** without explicit approval — shared by every module, wide blast radius.\n5. **Mediator handlers must be `public sealed`**, return `ValueTask<T>`, and `.ConfigureAwait(false)` every await.\n6. **Structured logging only** — no string interpolation in log messages; use message templates / `[LoggerMessage]`.\n7. **Propagate `CancellationToken`** into every EF/IO call; add as `= default` on public service methods.\n8. **Every command handler + paginated query handler needs a validator** (`{Name}Validator`). Enforced by `Architecture.Tests`.\n9. **Frontend: pass per-call data through `mutate(arg)`**, never via state the mutation callbacks close over (execute-time race). See `frontend/shared.md`.\n10. **Docs + changelog travel with the change** — a user-facing change (feature, endpoint, config, infra, breaking change) isn't done until the **separate docs repo** (`github.com/fullstackhero/docs`, the Astro site) is updated to match **and** a changelog entry is added (`src/content/docs/changelog/`). Don't let the docs drift from the code.\n\n## Rules index — read the relevant file before you work\n\n**Backend / cross-cutting** (`.agents/rules/`)\n\n| Working on… | Read |\n|---|---|\n| Module structure, boundaries, registration, DI, middleware order, config | `architecture.md` |\n| Endpoints, CQRS, validation, exceptions, permissions, versioning | `api-conventions.md` |\n| EF Core, entities, migrations, tenant isolation, query filters | `database.md` |\n| Cross-module events, Outbox/Inbox, idempotent handlers | `eventing.md` |\n| Caching (HybridCache/Redis), keys, invalidation | `caching.md` |\n| Background jobs (Hangfire), recurring jobs | `jobs.md` |\n| Outbound HTTP resilience (Polly) | `resilience.md` |\n| Files/blobs, presigned uploads, providers | `storage.md` |\n| CORS, security headers, rate limiting, idempotency, quotas | `security.md` |\n| SignalR / SSE backend | `realtime.md` |\n| Logging, correlation, OpenTelemetry | `logging.md` |\n| Unit test conventions, NetArchTest | `testing.md` |\n| Integration tests (Testcontainers harness + gotchas) | `integration-testing.md` |\n| **Modifying `src/BuildingBlocks`** (read first — it's protected) | `buildingblocks-protection.md` |\n| A specific module's quirks | `modules/{module}.md` (identity, multitenancy, chat, files, webhooks, auditing, billing, catalog, tickets, notifications) |\n\n**Frontend** (`.agents/rules/frontend/`)\n\n| Working on… | Read |\n|---|---|\n| Any React work (shared stack, API client, Query, Tailwind, design language) | `frontend/shared.md` |\n| The operator app (`clients/admin`) | `frontend/admin.md` |\n| The tenant app (`clients/dashboard`) | `frontend/dashboard.md` |\n\n## Coding style (backend)\n\nFile-scoped namespaces · 4-space indent · explicit types (`var` only when RHS-obvious) · `is null` /\n`is not null` · pattern matching + switch expressions · `ArgumentNullException.ThrowIfNull` guards ·\nrecords for DTOs/events/value objects · `default!` for required non-nullable strings. Build runs with\n`TreatWarningsAsErrors` — warnings fail the build.\n\n## Adding things (quick pointers)\n\n- **Feature** — Contracts command/query → handler → validator → endpoint → wire in module `MapEndpoints()` → tests. Details: `api-conventions.md`.\n- **Module** — new `Modules.{Name}` + `.Contracts`, implement `IModule` w/ assembly-level `[assembly: FshModule(typeof(XModule), order)]`, register in **all four places**, add migration folder + tests. Details: `architecture.md`.\n- **React page** — API module (`src/api/`) → page → register lazy route → (admin) mirror permission + RouteGuard → Playwright test. Details: `frontend/shared.md`.\n\n## AI tooling resources\n\n- **Rules** — `.agents/rules/*.md` (indexed above). Read on demand.\n- **Skills** — `.agents/skills/*/SKILL.md`: step-by-step task recipes. Scaffolders: `add-feature`, `add-entity`, `add-module`, `add-react-page`, `add-full-slice`. Ops: `create-migration`, `add-integration-event`, `add-permission`. Reference: `query-patterns`, `testing-guide`, `mediator-reference`.\n- **Workflows** — `.agents/workflows/*.md`: task playbooks (`code-reviewer`, `feature-scaffolder`, `module-creator`, `architecture-guard`, `migration-helper`).\n","GEMINI.md":"# Gemini CLI\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n",".agents/skills/add-entity/SKILL.md":"---\nname: add-entity\ndescription: Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.\nargument-hint: \"[ModuleName] [EntityName]\"\n---\n\n# Add Entity\n\nRich domain model: `sealed` aggregate, private EF ctor, static factory, behavior via methods, domain\nevents. DB conventions: `.agents/rules/database.md`.\n\n## Entity — `AggregateRoot<Guid>` (or `BaseEntity<Guid>`)\n\n`BaseEntity<TId>` gives only `Id` + domain-event machinery. Audit/tenant/soft-delete are **opt-in via\nmarker interfaces** (the base does NOT carry those fields). New ids use **`Guid.CreateVersion7()`**.\n\n```csharp\npublic sealed class {Entity} : AggregateRoot<Guid>, IHasTenant, IAuditableEntity, ISoftDeletable\n{\n    public string Name { get; private set; } = default!;\n    public Money Price { get; private set; } = default!;\n\n    // IHasTenant\n    public string TenantId { get; private set; } = default!;\n    // IAuditableEntity\n    public DateTimeOffset CreatedOnUtc { get; set; }\n    public string? CreatedBy { get; set; }\n    public DateTimeOffset? LastModifiedOnUtc { get; set; }\n    public string? LastModifiedBy { get; set; }\n    // ISoftDeletable\n    public bool IsDeleted { get; set; }\n    public DateTimeOffset? DeletedOnUtc { get; set; }\n    public string? DeletedBy { get; set; }\n\n    private {Entity}() { }   // EF\n\n    public static {Entity} Create(string name, Money price)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        ArgumentNullException.ThrowIfNull(price);\n\n        var entity = new {Entity} { Id = Guid.CreateVersion7(), Name = name.Trim(), Price = price };\n        entity.AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new {Entity}CreatedDomainEvent(entity.Id, entity.Name, id, ts)));\n        return entity;\n    }\n\n    public void Rename(string name)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        Name = name.Trim();\n    }\n}\n```\n\nNotes: setters are `private set`; `TenantId`/audit/soft-delete members are settable by the framework\n(interceptor + Finbuckle) so they aren't `private set`. Use `Guid.CreateVersion7()`, never `Guid.NewGuid()`.\n\n## Domain event — inherit `DomainEvent` (abstract record)\n\n```csharp\npublic sealed record {Entity}CreatedDomainEvent(\n    Guid {Entity}Id, string Name, Guid EventId, DateTimeOffset OccurredOnUtc)\n    : DomainEvent(EventId, OccurredOnUtc);\n```\n\nRaise with the `DomainEvent.Create((id, ts) => …)` helper + `AddDomainEvent(...)` (not `QueueDomainEvent`).\n\n## EF configuration\n\n```csharp\npublic sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>\n{\n    public void Configure(EntityTypeBuilder<{Entity}> builder)\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n        builder.ToTable(\"{Entities}\");                       // schema is set once on the DbContext\n        builder.HasKey(x => x.Id);\n        builder.Property(x => x.Name).IsRequired().HasMaxLength(200);\n\n        // soft-deletable unique field → filter on live rows only\n        builder.HasIndex(x => x.Name).IsUnique().HasFilter(\"\\\"IsDeleted\\\" = FALSE\");\n\n        // owned value object\n        builder.OwnsOne(x => x.Price, m =>\n        {\n            m.Property(p => p.Amount).HasColumnName(\"PriceAmount\").HasPrecision(18, 4);\n            m.Property(p => p.Currency).HasColumnName(\"PriceCurrency\").HasMaxLength(3);\n        });\n\n        builder.Ignore(x => x.DomainEvents);\n    }\n}\n```\n\n- **Do NOT add a manual `HasQueryFilter` for soft-delete or tenant** — `BaseDbContext` applies both automatically.\n- A child entity reached only via a parent nav-collection needs `builder.Property(x => x.Id).ValueGeneratedNever()` in **its** config, or EF inserts it as `Modified` → 0-row UPDATE. See `database.md`.\n\n## Register in the module DbContext\n\nAdd a `DbSet`; configurations are picked up by `ApplyConfigurationsFromAssembly`:\n\n```csharp\npublic DbSet<{Entity}> {Entities} => Set<{Entity}>();\n```\n\nThe DbContext already extends `BaseDbContext` and calls `base.OnModelCreating` **last** — don't change that.\n\n## Migration\n\nUse the **create-migration** skill (build first, correct `--context`):\n\n```bash\ndotnet ef migrations add Add{Entity} \\\n  --project src/Host/FSH.Starter.Migrations.PostgreSQL \\\n  --startup-project src/Host/FSH.Starter.Api \\\n  --context {X}DbContext\n```\n\n## Checklist\n\n- [ ] `sealed`, `AggregateRoot<Guid>` (+ `IHasTenant`/`IAuditableEntity`/`ISoftDeletable` as needed), private ctor, static `Create` using `Guid.CreateVersion7()`\n- [ ] Domain event inherits `DomainEvent`; raised via `DomainEvent.Create` + `AddDomainEvent`\n- [ ] EF config: no manual soft-delete/tenant filter; `ValueGeneratedNever()` on nav-collection children\n- [ ] `DbSet` added; build green; migration created with `--context {X}DbContext`\n",".agents/skills/add-feature/SKILL.md":"---\nname: add-feature\ndescription: Add a vertical-slice feature (command/query + handler + validator + endpoint) to an existing FSH module. Use when adding an API endpoint or business operation to a module that already exists.\nargument-hint: \"[ModuleName] [Area] [FeatureName]\"\n---\n\n# Add Feature\n\nA feature is a vertical slice **split across two projects**: the request/response types live in the\nmodule's `.Contracts` project (public API); the handler, validator, and endpoint live in the runtime\nproject. Full conventions: `.agents/rules/api-conventions.md`.\n\n## Layout (real)\n\n```\nsrc/Modules/{X}/Modules.{X}.Contracts/v1/{Area}/{Feature}Command.cs   # ICommand<T>/IQuery<T>\nsrc/Modules/{X}/Modules.{X}.Contracts/Dtos/{Entity}Dto.cs             # response DTOs (if any)\nsrc/Modules/{X}/Modules.{X}/Features/v1/{Area}/{Feature}/\n├── {Feature}CommandHandler.cs    # public sealed, injects the DbContext directly\n├── {Feature}CommandValidator.cs  # required for commands + paginated queries\n└── {Feature}Endpoint.cs          # internal static extension\n```\n\n## Step 1 — Command/Query (Contracts project)\n\n`Mediator` interfaces (`using Mediator;`). Records. A create command can return the raw `Guid`.\n\n```csharp\nnamespace FSH.Modules.{X}.Contracts.v1.{Area};\n\npublic sealed record Create{Entity}Command(string Name, decimal PriceAmount, string PriceCurrency)\n    : ICommand<Guid>;\n```\n\nRead/list DTOs go in `Modules.{X}.Contracts/Dtos/`. Paginated queries return `PagedResponse<T>`\n(`FSH.Framework.Shared.Persistence`) — see `query-patterns`.\n\n## Step 2 — Handler (runtime `Features/`) — inject the DbContext, NOT a repository\n\nThere is **no generic `IRepository<T>`**. Inject the module's `{X}DbContext`. `public sealed`, primary\nctor, `ValueTask<T>`, `.ConfigureAwait(false)`, guard first. Tenant/audit fields are auto-stamped — only\ninject `ICurrentUser` if you need the acting user (`GetUserId()` / `GetTenant()`).\n\n```csharp\npublic sealed class Create{Entity}CommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<Create{Entity}Command, Guid>\n{\n    public async ValueTask<Guid> Handle(Create{Entity}Command command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var entity = {Entity}.Create(command.Name, new Money(command.PriceAmount, command.PriceCurrency));\n        dbContext.{Entities}.Add(entity);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return entity.Id;\n    }\n}\n```\n\nThrow `NotFoundException` / `CustomException(msg, errors, HttpStatusCode)` (`FSH.Framework.Core.Exceptions`) — the global handler maps them to ProblemDetails.\n\n## Step 3 — Validator (required; same folder)\n\n```csharp\npublic sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>\n{\n    public Create{Entity}CommandValidator()\n    {\n        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);\n        RuleFor(x => x.PriceCurrency).NotEmpty().Length(3);\n    }\n}\n```\n\n`Architecture.Tests` fails the build if a command/paginated-query handler has no `{Name}Validator`.\n\n## Step 4 — Endpoint (same folder)\n\n```csharp\npublic static class Create{Entity}Endpoint\n{\n    internal static RouteHandlerBuilder MapCreate{Entity}Endpoint(this IEndpointRouteBuilder endpoints) =>\n        endpoints.MapPost(\"/{entities}\",\n                async (Create{Entity}Command command, IMediator mediator, CancellationToken ct) =>\n                    Results.Ok(await mediator.Send(command, ct)))\n            .WithName(\"Create{Entity}\")\n            .WithSummary(\"Create a {entity}\")\n            .RequirePermission({X}Permissions.{Entities}.Create)\n            .WithIdempotency();   // on replay-safe POSTs\n}\n```\n\n## Step 5 — Wire it in `{X}Module.MapEndpoints`\n\n```csharp\ngroup.MapCreate{Entity}Endpoint();   // group = endpoints.MapGroup(\"api/v{version:apiVersion}/{x}\") …\n```\n\n## Step 6 — Verify\n\n```bash\ndotnet build src/FSH.Starter.slnx          # 0 warnings (TreatWarningsAsErrors)\ndotnet test src/Tests/{X}.Tests            # + add a handler/validator test (see testing-guide)\n```\n\n## Checklist\n\n- [ ] Command/Query in the **Contracts** project (`using Mediator;`), DTOs in `Contracts/Dtos/`\n- [ ] Handler `public sealed`, injects `{X}DbContext` (no repository), `ValueTask<T>` + `.ConfigureAwait(false)`\n- [ ] `{Name}Validator` exists\n- [ ] Endpoint `internal static …Map{Feature}Endpoint`, `.RequirePermission(...)`, `.WithName/.WithSummary`\n- [ ] Wired in `{X}Module.MapEndpoints`\n- [ ] Build 0 warnings; test added\n"},"files":{"CLAUDE.md":"# Claude Code\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n","AGENTS.md":"# FullStackHero .NET Starter Kit\n\n> A production-ready modular .NET 10 monolith + two React 19 apps, built for enterprise SaaS.\n\nThis file is the canonical guide for **all** AI coding tools (Claude Code, Gemini CLI, Cursor, Codex, …).\n`CLAUDE.md` and `GEMINI.md` are thin bridges that import this file — edit conventions **here**, not there.\n\nThis file is the map. Detailed conventions live in `.agents/rules/` and are read on demand — **read the\nrelevant rule file before working in that area** (see the index below). Keep this file lean.\n\n## What this is\n\nA **modular monolith** (Vertical Slice Architecture) backend that ships with two **React + Vite**\nfront-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and more are first-class.\n\n- **Backend** — .NET 10, EF Core 10, PostgreSQL, Redis, JWT + ASP.NET Identity, Finbuckle multitenancy,\n  Hangfire, OpenAPI/Scalar, Serilog + OpenTelemetry, .NET Aspire.\n- **Frontends** — `clients/admin` (operator-facing) and `clients/dashboard` (tenant-facing): React 19,\n  Vite 7, TypeScript, TanStack Query v5, React Router 7, Radix + Tailwind v4 (shadcn-style), SignalR/SSE.\n\n## Repo map\n\n| Path | What |\n|------|------|\n| `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…). **Protected — see below.** |\n| `src/Modules/{Name}/` | Bounded contexts. Each has a runtime project + a `.Contracts` project (its only public API). |\n| `src/Host/FSH.Starter.Api` | Composition-root Web API host. |\n| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Redis, MinIO, migrator, API, **both React apps**). |\n| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner. DB is **not** migrated at API startup. |\n| `src/Host/FSH.Starter.Migrations.PostgreSQL` | All EF migrations, organized per-module by folder. |\n| `src/Tests/` | Per-module tests, `Architecture.Tests` (NetArchTest), `Integration.Tests` (Testcontainers). |\n| `src/Tools/CLI` | The `fsh` CLI (Spectre.Console). |\n| `clients/admin`, `clients/dashboard` | The two React apps. |\n| `deploy/` | Infra (docker, terraform, dokploy). |\n\n## Tech stack\n\n| Backend | | Frontend | |\n|---|---|---|---|\n| Framework | .NET 10 / C# latest | Framework | React 19 + Vite 7 + TS 5.x |\n| CQRS | Mediator 3.x (source-gen) | Data | TanStack Query v5 |\n| Validation | FluentValidation 12.x | Routing | React Router 7 |\n| ORM / DB | EF Core 10 / PostgreSQL (Npgsql) | UI | Radix + Tailwind v4 + CVA (shadcn) |\n| Auth | JWT Bearer + ASP.NET Identity | Forms | react-hook-form + zod (**admin only**) |\n| Multitenancy | Finbuckle 10.x | Realtime | `@microsoft/signalr`, SSE (dashboard) |\n| Cache / Jobs | Redis, Hangfire | Tests | Playwright (route-mocked) |\n| Docs | OpenAPI + Scalar | API client | hand-written `apiFetch` (no codegen) |\n| Hosting | .NET Aspire | Env | runtime `/config.json` (not `VITE_*`) |\n| Testing | xUnit, Shouldly, NSubstitute, AutoFixture, NetArchTest, Testcontainers | | |\n\n## Build & run\n\n```bash\n# Whole stack (Postgres + pgAdmin + Redis + MinIO + migrator + API + both React apps)\ndotnet run --project src/Host/FSH.Starter.AppHost   # one-time: npm install in clients/admin & clients/dashboard\n\ndotnet build src/FSH.Starter.slnx                   # build backend\ndotnet run --project src/Host/FSH.Starter.Api       # API only → https://localhost:7030 (/scalar)\ndotnet test src/FSH.Starter.slnx                    # tests — integration tests REQUIRE Docker\n\ncd clients/admin && npm install && npm run dev       # → http://localhost:5173\ncd clients/dashboard && npm install && npm run dev   # → http://localhost:5174\n```\n\nMigrations / seed (DbMigrator, separate step):\n```bash\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- apply [--seed]\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending\n```\n\n**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · MinIO 9000/9001.\n\n## Branching & PRs\n\nSingle long-lived branch: **`main`** (the default) — there is **no `develop`**. Branch from and target `main`; stable releases are cut from `v*` tags. CI is split into path-scoped **Backend CI** (`src/**`) and **Frontend CI** (`clients/**`) workflows; branch protection requires only those two gate checks — never the individual jobs, which are skipped on the other side's PRs.\n\n## Golden rules (do not break)\n\n1. **Module boundaries** — a module references another module only through its `.Contracts` project, never its runtime project. Enforced by `Architecture.Tests`.\n2. **Registering a module touches FOUR places** — `Program.cs` Mediator `o.Assemblies` (two markers each) + `moduleAssemblies` array, **and the identical pair in `DbMigrator/Program.cs`**. A missing Mediator marker = handlers silently undiscovered. See `architecture.md`.\n3. **Tenant isolation is default-ON** via `BaseDbContext`. Opt out only via `IGlobalEntity`. Subclass DbContexts call `base.OnModelCreating` **last**. See `database.md`.\n4. **Do NOT modify `src/BuildingBlocks`** without explicit approval — shared by every module, wide blast radius.\n5. **Mediator handlers must be `public sealed`**, return `ValueTask<T>`, and `.ConfigureAwait(false)` every await.\n6. **Structured logging only** — no string interpolation in log messages; use message templates / `[LoggerMessage]`.\n7. **Propagate `CancellationToken`** into every EF/IO call; add as `= default` on public service methods.\n8. **Every command handler + paginated query handler needs a validator** (`{Name}Validator`). Enforced by `Architecture.Tests`.\n9. **Frontend: pass per-call data through `mutate(arg)`**, never via state the mutation callbacks close over (execute-time race). See `frontend/shared.md`.\n10. **Docs + changelog travel with the change** — a user-facing change (feature, endpoint, config, infra, breaking change) isn't done until the **separate docs repo** (`github.com/fullstackhero/docs`, the Astro site) is updated to match **and** a changelog entry is added (`src/content/docs/changelog/`). Don't let the docs drift from the code.\n\n## Rules index — read the relevant file before you work\n\n**Backend / cross-cutting** (`.agents/rules/`)\n\n| Working on… | Read |\n|---|---|\n| Module structure, boundaries, registration, DI, middleware order, config | `architecture.md` |\n| Endpoints, CQRS, validation, exceptions, permissions, versioning | `api-conventions.md` |\n| EF Core, entities, migrations, tenant isolation, query filters | `database.md` |\n| Cross-module events, Outbox/Inbox, idempotent handlers | `eventing.md` |\n| Caching (HybridCache/Redis), keys, invalidation | `caching.md` |\n| Background jobs (Hangfire), recurring jobs | `jobs.md` |\n| Outbound HTTP resilience (Polly) | `resilience.md` |\n| Files/blobs, presigned uploads, providers | `storage.md` |\n| CORS, security headers, rate limiting, idempotency, quotas | `security.md` |\n| SignalR / SSE backend | `realtime.md` |\n| Logging, correlation, OpenTelemetry | `logging.md` |\n| Unit test conventions, NetArchTest | `testing.md` |\n| Integration tests (Testcontainers harness + gotchas) | `integration-testing.md` |\n| **Modifying `src/BuildingBlocks`** (read first — it's protected) | `buildingblocks-protection.md` |\n| A specific module's quirks | `modules/{module}.md` (identity, multitenancy, chat, files, webhooks, auditing, billing, catalog, tickets, notifications) |\n\n**Frontend** (`.agents/rules/frontend/`)\n\n| Working on… | Read |\n|---|---|\n| Any React work (shared stack, API client, Query, Tailwind, design language) | `frontend/shared.md` |\n| The operator app (`clients/admin`) | `frontend/admin.md` |\n| The tenant app (`clients/dashboard`) | `frontend/dashboard.md` |\n\n## Coding style (backend)\n\nFile-scoped namespaces · 4-space indent · explicit types (`var` only when RHS-obvious) · `is null` /\n`is not null` · pattern matching + switch expressions · `ArgumentNullException.ThrowIfNull` guards ·\nrecords for DTOs/events/value objects · `default!` for required non-nullable strings. Build runs with\n`TreatWarningsAsErrors` — warnings fail the build.\n\n## Adding things (quick pointers)\n\n- **Feature** — Contracts command/query → handler → validator → endpoint → wire in module `MapEndpoints()` → tests. Details: `api-conventions.md`.\n- **Module** — new `Modules.{Name}` + `.Contracts`, implement `IModule` w/ assembly-level `[assembly: FshModule(typeof(XModule), order)]`, register in **all four places**, add migration folder + tests. Details: `architecture.md`.\n- **React page** — API module (`src/api/`) → page → register lazy route → (admin) mirror permission + RouteGuard → Playwright test. Details: `frontend/shared.md`.\n\n## AI tooling resources\n\n- **Rules** — `.agents/rules/*.md` (indexed above). Read on demand.\n- **Skills** — `.agents/skills/*/SKILL.md`: step-by-step task recipes. Scaffolders: `add-feature`, `add-entity`, `add-module`, `add-react-page`, `add-full-slice`. Ops: `create-migration`, `add-integration-event`, `add-permission`. Reference: `query-patterns`, `testing-guide`, `mediator-reference`.\n- **Workflows** — `.agents/workflows/*.md`: task playbooks (`code-reviewer`, `feature-scaffolder`, `module-creator`, `architecture-guard`, `migration-helper`).\n","GEMINI.md":"# Gemini CLI\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n",".agents/skills/add-entity/SKILL.md":"---\nname: add-entity\ndescription: Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.\nargument-hint: \"[ModuleName] [EntityName]\"\n---\n\n# Add Entity\n\nRich domain model: `sealed` aggregate, private EF ctor, static factory, behavior via methods, domain\nevents. DB conventions: `.agents/rules/database.md`.\n\n## Entity — `AggregateRoot<Guid>` (or `BaseEntity<Guid>`)\n\n`BaseEntity<TId>` gives only `Id` + domain-event machinery. Audit/tenant/soft-delete are **opt-in via\nmarker interfaces** (the base does NOT carry those fields). New ids use **`Guid.CreateVersion7()`**.\n\n```csharp\npublic sealed class {Entity} : AggregateRoot<Guid>, IHasTenant, IAuditableEntity, ISoftDeletable\n{\n    public string Name { get; private set; } = default!;\n    public Money Price { get; private set; } = default!;\n\n    // IHasTenant\n    public string TenantId { get; private set; } = default!;\n    // IAuditableEntity\n    public DateTimeOffset CreatedOnUtc { get; set; }\n    public string? CreatedBy { get; set; }\n    public DateTimeOffset? LastModifiedOnUtc { get; set; }\n    public string? LastModifiedBy { get; set; }\n    // ISoftDeletable\n    public bool IsDeleted { get; set; }\n    public DateTimeOffset? DeletedOnUtc { get; set; }\n    public string? DeletedBy { get; set; }\n\n    private {Entity}() { }   // EF\n\n    public static {Entity} Create(string name, Money price)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        ArgumentNullException.ThrowIfNull(price);\n\n        var entity = new {Entity} { Id = Guid.CreateVersion7(), Name = name.Trim(), Price = price };\n        entity.AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new {Entity}CreatedDomainEvent(entity.Id, entity.Name, id, ts)));\n        return entity;\n    }\n\n    public void Rename(string name)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        Name = name.Trim();\n    }\n}\n```\n\nNotes: setters are `private set`; `TenantId`/audit/soft-delete members are settable by the framework\n(interceptor + Finbuckle) so they aren't `private set`. Use `Guid.CreateVersion7()`, never `Guid.NewGuid()`.\n\n## Domain event — inherit `DomainEvent` (abstract record)\n\n```csharp\npublic sealed record {Entity}CreatedDomainEvent(\n    Guid {Entity}Id, string Name, Guid EventId, DateTimeOffset OccurredOnUtc)\n    : DomainEvent(EventId, OccurredOnUtc);\n```\n\nRaise with the `DomainEvent.Create((id, ts) => …)` helper + `AddDomainEvent(...)` (not `QueueDomainEvent`).\n\n## EF configuration\n\n```csharp\npublic sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>\n{\n    public void Configure(EntityTypeBuilder<{Entity}> builder)\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n        builder.ToTable(\"{Entities}\");                       // schema is set once on the DbContext\n        builder.HasKey(x => x.Id);\n        builder.Property(x => x.Name).IsRequired().HasMaxLength(200);\n\n        // soft-deletable unique field → filter on live rows only\n        builder.HasIndex(x => x.Name).IsUnique().HasFilter(\"\\\"IsDeleted\\\" = FALSE\");\n\n        // owned value object\n        builder.OwnsOne(x => x.Price, m =>\n        {\n            m.Property(p => p.Amount).HasColumnName(\"PriceAmount\").HasPrecision(18, 4);\n            m.Property(p => p.Currency).HasColumnName(\"PriceCurrency\").HasMaxLength(3);\n        });\n\n        builder.Ignore(x => x.DomainEvents);\n    }\n}\n```\n\n- **Do NOT add a manual `HasQueryFilter` for soft-delete or tenant** — `BaseDbContext` applies both automatically.\n- A child entity reached only via a parent nav-collection needs `builder.Property(x => x.Id).ValueGeneratedNever()` in **its** config, or EF inserts it as `Modified` → 0-row UPDATE. See `database.md`.\n\n## Register in the module DbContext\n\nAdd a `DbSet`; configurations are picked up by `ApplyConfigurationsFromAssembly`:\n\n```csharp\npublic DbSet<{Entity}> {Entities} => Set<{Entity}>();\n```\n\nThe DbContext already extends `BaseDbContext` and calls `base.OnModelCreating` **last** — don't change that.\n\n## Migration\n\nUse the **create-migration** skill (build first, correct `--context`):\n\n```bash\ndotnet ef migrations add Add{Entity} \\\n  --project src/Host/FSH.Starter.Migrations.PostgreSQL \\\n  --startup-project src/Host/FSH.Starter.Api \\\n  --context {X}DbContext\n```\n\n## Checklist\n\n- [ ] `sealed`, `AggregateRoot<Guid>` (+ `IHasTenant`/`IAuditableEntity`/`ISoftDeletable` as needed), private ctor, static `Create` using `Guid.CreateVersion7()`\n- [ ] Domain event inherits `DomainEvent`; raised via `DomainEvent.Create` + `AddDomainEvent`\n- [ ] EF config: no manual soft-delete/tenant filter; `ValueGeneratedNever()` on nav-collection children\n- [ ] `DbSet` added; build green; migration created with `--context {X}DbContext`\n",".agents/skills/add-feature/SKILL.md":"---\nname: add-feature\ndescription: Add a vertical-slice feature (command/query + handler + validator + endpoint) to an existing FSH module. Use when adding an API endpoint or business operation to a module that already exists.\nargument-hint: \"[ModuleName] [Area] [FeatureName]\"\n---\n\n# Add Feature\n\nA feature is a vertical slice **split across two projects**: the request/response types live in the\nmodule's `.Contracts` project (public API); the handler, validator, and endpoint live in the runtime\nproject. Full conventions: `.agents/rules/api-conventions.md`.\n\n## Layout (real)\n\n```\nsrc/Modules/{X}/Modules.{X}.Contracts/v1/{Area}/{Feature}Command.cs   # ICommand<T>/IQuery<T>\nsrc/Modules/{X}/Modules.{X}.Contracts/Dtos/{Entity}Dto.cs             # response DTOs (if any)\nsrc/Modules/{X}/Modules.{X}/Features/v1/{Area}/{Feature}/\n├── {Feature}CommandHandler.cs    # public sealed, injects the DbContext directly\n├── {Feature}CommandValidator.cs  # required for commands + paginated queries\n└── {Feature}Endpoint.cs          # internal static extension\n```\n\n## Step 1 — Command/Query (Contracts project)\n\n`Mediator` interfaces (`using Mediator;`). Records. A create command can return the raw `Guid`.\n\n```csharp\nnamespace FSH.Modules.{X}.Contracts.v1.{Area};\n\npublic sealed record Create{Entity}Command(string Name, decimal PriceAmount, string PriceCurrency)\n    : ICommand<Guid>;\n```\n\nRead/list DTOs go in `Modules.{X}.Contracts/Dtos/`. Paginated queries return `PagedResponse<T>`\n(`FSH.Framework.Shared.Persistence`) — see `query-patterns`.\n\n## Step 2 — Handler (runtime `Features/`) — inject the DbContext, NOT a repository\n\nThere is **no generic `IRepository<T>`**. Inject the module's `{X}DbContext`. `public sealed`, primary\nctor, `ValueTask<T>`, `.ConfigureAwait(false)`, guard first. Tenant/audit fields are auto-stamped — only\ninject `ICurrentUser` if you need the acting user (`GetUserId()` / `GetTenant()`).\n\n```csharp\npublic sealed class Create{Entity}CommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<Create{Entity}Command, Guid>\n{\n    public async ValueTask<Guid> Handle(Create{Entity}Command command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var entity = {Entity}.Create(command.Name, new Money(command.PriceAmount, command.PriceCurrency));\n        dbContext.{Entities}.Add(entity);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return entity.Id;\n    }\n}\n```\n\nThrow `NotFoundException` / `CustomException(msg, errors, HttpStatusCode)` (`FSH.Framework.Core.Exceptions`) — the global handler maps them to ProblemDetails.\n\n## Step 3 — Validator (required; same folder)\n\n```csharp\npublic sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>\n{\n    public Create{Entity}CommandValidator()\n    {\n        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);\n        RuleFor(x => x.PriceCurrency).NotEmpty().Length(3);\n    }\n}\n```\n\n`Architecture.Tests` fails the build if a command/paginated-query handler has no `{Name}Validator`.\n\n## Step 4 — Endpoint (same folder)\n\n```csharp\npublic static class Create{Entity}Endpoint\n{\n    internal static RouteHandlerBuilder MapCreate{Entity}Endpoint(this IEndpointRouteBuilder endpoints) =>\n        endpoints.MapPost(\"/{entities}\",\n                async (Create{Entity}Command command, IMediator mediator, CancellationToken ct) =>\n                    Results.Ok(await mediator.Send(command, ct)))\n            .WithName(\"Create{Entity}\")\n            .WithSummary(\"Create a {entity}\")\n            .RequirePermission({X}Permissions.{Entities}.Create)\n            .WithIdempotency();   // on replay-safe POSTs\n}\n```\n\n## Step 5 — Wire it in `{X}Module.MapEndpoints`\n\n```csharp\ngroup.MapCreate{Entity}Endpoint();   // group = endpoints.MapGroup(\"api/v{version:apiVersion}/{x}\") …\n```\n\n## Step 6 — Verify\n\n```bash\ndotnet build src/FSH.Starter.slnx          # 0 warnings (TreatWarningsAsErrors)\ndotnet test src/Tests/{X}.Tests            # + add a handler/validator test (see testing-guide)\n```\n\n## Checklist\n\n- [ ] Command/Query in the **Contracts** project (`using Mediator;`), DTOs in `Contracts/Dtos/`\n- [ ] Handler `public sealed`, injects `{X}DbContext` (no repository), `ValueTask<T>` + `.ConfigureAwait(false)`\n- [ ] `{Name}Validator` exists\n- [ ] Endpoint `internal static …Map{Feature}Endpoint`, `.RequirePermission(...)`, `.WithName/.WithSummary`\n- [ ] Wired in `{X}Module.MapEndpoints`\n- [ ] Build 0 warnings; test added\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# Claude Code\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n","category":"root","tokens":37},{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# FullStackHero .NET Starter Kit\n\n> A production-ready modular .NET 10 monolith + two React 19 apps, built for enterprise SaaS.\n\nThis file is the canonical guide for **all** AI coding tools (Claude Code, Gemini CLI, Cursor, Codex, …).\n`CLAUDE.md` and `GEMINI.md` are thin bridges that import this file — edit conventions **here**, not there.\n\nThis file is the map. Detailed conventions live in `.agents/rules/` and are read on demand — **read the\nrelevant rule file before working in that area** (see the index below). Keep this file lean.\n\n## What this is\n\nA **modular monolith** (Vertical Slice Architecture) backend that ships with two **React + Vite**\nfront-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and more are first-class.\n\n- **Backend** — .NET 10, EF Core 10, PostgreSQL, Redis, JWT + ASP.NET Identity, Finbuckle multitenancy,\n  Hangfire, OpenAPI/Scalar, Serilog + OpenTelemetry, .NET Aspire.\n- **Frontends** — `clients/admin` (operator-facing) and `clients/dashboard` (tenant-facing): React 19,\n  Vite 7, TypeScript, TanStack Query v5, React Router 7, Radix + Tailwind v4 (shadcn-style), SignalR/SSE.\n\n## Repo map\n\n| Path | What |\n|------|------|\n| `src/BuildingBlocks/` | Shared framework libraries (Core, Persistence, Web, Caching, Eventing, Storage, Quota…). **Protected — see below.** |\n| `src/Modules/{Name}/` | Bounded contexts. Each has a runtime project + a `.Contracts` project (its only public API). |\n| `src/Host/FSH.Starter.Api` | Composition-root Web API host. |\n| `src/Host/FSH.Starter.AppHost` | .NET Aspire orchestrator (Postgres, Redis, MinIO, migrator, API, **both React apps**). |\n| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner. DB is **not** migrated at API startup. |\n| `src/Host/FSH.Starter.Migrations.PostgreSQL` | All EF migrations, organized per-module by folder. |\n| `src/Tests/` | Per-module tests, `Architecture.Tests` (NetArchTest), `Integration.Tests` (Testcontainers). |\n| `src/Tools/CLI` | The `fsh` CLI (Spectre.Console). |\n| `clients/admin`, `clients/dashboard` | The two React apps. |\n| `deploy/` | Infra (docker, terraform, dokploy). |\n\n## Tech stack\n\n| Backend | | Frontend | |\n|---|---|---|---|\n| Framework | .NET 10 / C# latest | Framework | React 19 + Vite 7 + TS 5.x |\n| CQRS | Mediator 3.x (source-gen) | Data | TanStack Query v5 |\n| Validation | FluentValidation 12.x | Routing | React Router 7 |\n| ORM / DB | EF Core 10 / PostgreSQL (Npgsql) | UI | Radix + Tailwind v4 + CVA (shadcn) |\n| Auth | JWT Bearer + ASP.NET Identity | Forms | react-hook-form + zod (**admin only**) |\n| Multitenancy | Finbuckle 10.x | Realtime | `@microsoft/signalr`, SSE (dashboard) |\n| Cache / Jobs | Redis, Hangfire | Tests | Playwright (route-mocked) |\n| Docs | OpenAPI + Scalar | API client | hand-written `apiFetch` (no codegen) |\n| Hosting | .NET Aspire | Env | runtime `/config.json` (not `VITE_*`) |\n| Testing | xUnit, Shouldly, NSubstitute, AutoFixture, NetArchTest, Testcontainers | | |\n\n## Build & run\n\n```bash\n# Whole stack (Postgres + pgAdmin + Redis + MinIO + migrator + API + both React apps)\ndotnet run --project src/Host/FSH.Starter.AppHost   # one-time: npm install in clients/admin & clients/dashboard\n\ndotnet build src/FSH.Starter.slnx                   # build backend\ndotnet run --project src/Host/FSH.Starter.Api       # API only → https://localhost:7030 (/scalar)\ndotnet test src/FSH.Starter.slnx                    # tests — integration tests REQUIRE Docker\n\ncd clients/admin && npm install && npm run dev       # → http://localhost:5173\ncd clients/dashboard && npm install && npm run dev   # → http://localhost:5174\n```\n\nMigrations / seed (DbMigrator, separate step):\n```bash\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- apply [--seed]\ndotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending\n```\n\n**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · MinIO 9000/9001.\n\n## Branching & PRs\n\nSingle long-lived branch: **`main`** (the default) — there is **no `develop`**. Branch from and target `main`; stable releases are cut from `v*` tags. CI is split into path-scoped **Backend CI** (`src/**`) and **Frontend CI** (`clients/**`) workflows; branch protection requires only those two gate checks — never the individual jobs, which are skipped on the other side's PRs.\n\n## Golden rules (do not break)\n\n1. **Module boundaries** — a module references another module only through its `.Contracts` project, never its runtime project. Enforced by `Architecture.Tests`.\n2. **Registering a module touches FOUR places** — `Program.cs` Mediator `o.Assemblies` (two markers each) + `moduleAssemblies` array, **and the identical pair in `DbMigrator/Program.cs`**. A missing Mediator marker = handlers silently undiscovered. See `architecture.md`.\n3. **Tenant isolation is default-ON** via `BaseDbContext`. Opt out only via `IGlobalEntity`. Subclass DbContexts call `base.OnModelCreating` **last**. See `database.md`.\n4. **Do NOT modify `src/BuildingBlocks`** without explicit approval — shared by every module, wide blast radius.\n5. **Mediator handlers must be `public sealed`**, return `ValueTask<T>`, and `.ConfigureAwait(false)` every await.\n6. **Structured logging only** — no string interpolation in log messages; use message templates / `[LoggerMessage]`.\n7. **Propagate `CancellationToken`** into every EF/IO call; add as `= default` on public service methods.\n8. **Every command handler + paginated query handler needs a validator** (`{Name}Validator`). Enforced by `Architecture.Tests`.\n9. **Frontend: pass per-call data through `mutate(arg)`**, never via state the mutation callbacks close over (execute-time race). See `frontend/shared.md`.\n10. **Docs + changelog travel with the change** — a user-facing change (feature, endpoint, config, infra, breaking change) isn't done until the **separate docs repo** (`github.com/fullstackhero/docs`, the Astro site) is updated to match **and** a changelog entry is added (`src/content/docs/changelog/`). Don't let the docs drift from the code.\n\n## Rules index — read the relevant file before you work\n\n**Backend / cross-cutting** (`.agents/rules/`)\n\n| Working on… | Read |\n|---|---|\n| Module structure, boundaries, registration, DI, middleware order, config | `architecture.md` |\n| Endpoints, CQRS, validation, exceptions, permissions, versioning | `api-conventions.md` |\n| EF Core, entities, migrations, tenant isolation, query filters | `database.md` |\n| Cross-module events, Outbox/Inbox, idempotent handlers | `eventing.md` |\n| Caching (HybridCache/Redis), keys, invalidation | `caching.md` |\n| Background jobs (Hangfire), recurring jobs | `jobs.md` |\n| Outbound HTTP resilience (Polly) | `resilience.md` |\n| Files/blobs, presigned uploads, providers | `storage.md` |\n| CORS, security headers, rate limiting, idempotency, quotas | `security.md` |\n| SignalR / SSE backend | `realtime.md` |\n| Logging, correlation, OpenTelemetry | `logging.md` |\n| Unit test conventions, NetArchTest | `testing.md` |\n| Integration tests (Testcontainers harness + gotchas) | `integration-testing.md` |\n| **Modifying `src/BuildingBlocks`** (read first — it's protected) | `buildingblocks-protection.md` |\n| A specific module's quirks | `modules/{module}.md` (identity, multitenancy, chat, files, webhooks, auditing, billing, catalog, tickets, notifications) |\n\n**Frontend** (`.agents/rules/frontend/`)\n\n| Working on… | Read |\n|---|---|\n| Any React work (shared stack, API client, Query, Tailwind, design language) | `frontend/shared.md` |\n| The operator app (`clients/admin`) | `frontend/admin.md` |\n| The tenant app (`clients/dashboard`) | `frontend/dashboard.md` |\n\n## Coding style (backend)\n\nFile-scoped namespaces · 4-space indent · explicit types (`var` only when RHS-obvious) · `is null` /\n`is not null` · pattern matching + switch expressions · `ArgumentNullException.ThrowIfNull` guards ·\nrecords for DTOs/events/value objects · `default!` for required non-nullable strings. Build runs with\n`TreatWarningsAsErrors` — warnings fail the build.\n\n## Adding things (quick pointers)\n\n- **Feature** — Contracts command/query → handler → validator → endpoint → wire in module `MapEndpoints()` → tests. Details: `api-conventions.md`.\n- **Module** — new `Modules.{Name}` + `.Contracts`, implement `IModule` w/ assembly-level `[assembly: FshModule(typeof(XModule), order)]`, register in **all four places**, add migration folder + tests. Details: `architecture.md`.\n- **React page** — API module (`src/api/`) → page → register lazy route → (admin) mirror permission + RouteGuard → Playwright test. Details: `frontend/shared.md`.\n\n## AI tooling resources\n\n- **Rules** — `.agents/rules/*.md` (indexed above). Read on demand.\n- **Skills** — `.agents/skills/*/SKILL.md`: step-by-step task recipes. Scaffolders: `add-feature`, `add-entity`, `add-module`, `add-react-page`, `add-full-slice`. Ops: `create-migration`, `add-integration-event`, `add-permission`. Reference: `query-patterns`, `testing-guide`, `mediator-reference`.\n- **Workflows** — `.agents/workflows/*.md`: task playbooks (`code-reviewer`, `feature-scaffolder`, `module-creator`, `architecture-guard`, `migration-helper`).\n","category":"root","tokens":2298},{"name":"GEMINI.md","path":"GEMINI.md","title":"GEMINI.md","content":"# Gemini CLI\n\nThe canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.\n\n@AGENTS.md\n","category":"root","tokens":37},{"name":"SKILL.md","path":".agents/skills/add-entity/SKILL.md","title":"add-entity Skill","content":"---\nname: add-entity\ndescription: Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.\nargument-hint: \"[ModuleName] [EntityName]\"\n---\n\n# Add Entity\n\nRich domain model: `sealed` aggregate, private EF ctor, static factory, behavior via methods, domain\nevents. DB conventions: `.agents/rules/database.md`.\n\n## Entity — `AggregateRoot<Guid>` (or `BaseEntity<Guid>`)\n\n`BaseEntity<TId>` gives only `Id` + domain-event machinery. Audit/tenant/soft-delete are **opt-in via\nmarker interfaces** (the base does NOT carry those fields). New ids use **`Guid.CreateVersion7()`**.\n\n```csharp\npublic sealed class {Entity} : AggregateRoot<Guid>, IHasTenant, IAuditableEntity, ISoftDeletable\n{\n    public string Name { get; private set; } = default!;\n    public Money Price { get; private set; } = default!;\n\n    // IHasTenant\n    public string TenantId { get; private set; } = default!;\n    // IAuditableEntity\n    public DateTimeOffset CreatedOnUtc { get; set; }\n    public string? CreatedBy { get; set; }\n    public DateTimeOffset? LastModifiedOnUtc { get; set; }\n    public string? LastModifiedBy { get; set; }\n    // ISoftDeletable\n    public bool IsDeleted { get; set; }\n    public DateTimeOffset? DeletedOnUtc { get; set; }\n    public string? DeletedBy { get; set; }\n\n    private {Entity}() { }   // EF\n\n    public static {Entity} Create(string name, Money price)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        ArgumentNullException.ThrowIfNull(price);\n\n        var entity = new {Entity} { Id = Guid.CreateVersion7(), Name = name.Trim(), Price = price };\n        entity.AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new {Entity}CreatedDomainEvent(entity.Id, entity.Name, id, ts)));\n        return entity;\n    }\n\n    public void Rename(string name)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        Name = name.Trim();\n    }\n}\n```\n\nNotes: setters are `private set`; `TenantId`/audit/soft-delete members are settable by the framework\n(interceptor + Finbuckle) so they aren't `private set`. Use `Guid.CreateVersion7()`, never `Guid.NewGuid()`.\n\n## Domain event — inherit `DomainEvent` (abstract record)\n\n```csharp\npublic sealed record {Entity}CreatedDomainEvent(\n    Guid {Entity}Id, string Name, Guid EventId, DateTimeOffset OccurredOnUtc)\n    : DomainEvent(EventId, OccurredOnUtc);\n```\n\nRaise with the `DomainEvent.Create((id, ts) => …)` helper + `AddDomainEvent(...)` (not `QueueDomainEvent`).\n\n## EF configuration\n\n```csharp\npublic sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}>\n{\n    public void Configure(EntityTypeBuilder<{Entity}> builder)\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n        builder.ToTable(\"{Entities}\");                       // schema is set once on the DbContext\n        builder.HasKey(x => x.Id);\n        builder.Property(x => x.Name).IsRequired().HasMaxLength(200);\n\n        // soft-deletable unique field → filter on live rows only\n        builder.HasIndex(x => x.Name).IsUnique().HasFilter(\"\\\"IsDeleted\\\" = FALSE\");\n\n        // owned value object\n        builder.OwnsOne(x => x.Price, m =>\n        {\n            m.Property(p => p.Amount).HasColumnName(\"PriceAmount\").HasPrecision(18, 4);\n            m.Property(p => p.Currency).HasColumnName(\"PriceCurrency\").HasMaxLength(3);\n        });\n\n        builder.Ignore(x => x.DomainEvents);\n    }\n}\n```\n\n- **Do NOT add a manual `HasQueryFilter` for soft-delete or tenant** — `BaseDbContext` applies both automatically.\n- A child entity reached only via a parent nav-collection needs `builder.Property(x => x.Id).ValueGeneratedNever()` in **its** config, or EF inserts it as `Modified` → 0-row UPDATE. See `database.md`.\n\n## Register in the module DbContext\n\nAdd a `DbSet`; configurations are picked up by `ApplyConfigurationsFromAssembly`:\n\n```csharp\npublic DbSet<{Entity}> {Entities} => Set<{Entity}>();\n```\n\nThe DbContext already extends `BaseDbContext` and calls `base.OnModelCreating` **last** — don't change that.\n\n## Migration\n\nUse the **create-migration** skill (build first, correct `--context`):\n\n```bash\ndotnet ef migrations add Add{Entity} \\\n  --project src/Host/FSH.Starter.Migrations.PostgreSQL \\\n  --startup-project src/Host/FSH.Starter.Api \\\n  --context {X}DbContext\n```\n\n## Checklist\n\n- [ ] `sealed`, `AggregateRoot<Guid>` (+ `IHasTenant`/`IAuditableEntity`/`ISoftDeletable` as needed), private ctor, static `Create` using `Guid.CreateVersion7()`\n- [ ] Domain event inherits `DomainEvent`; raised via `DomainEvent.Create` + `AddDomainEvent`\n- [ ] EF config: no manual soft-delete/tenant filter; `ValueGeneratedNever()` on nav-collection children\n- [ ] `DbSet` added; build green; migration created with `--context {X}DbContext`\n","category":".agents","tokens":1209},{"name":"SKILL.md","path":".agents/skills/add-feature/SKILL.md","title":"add-feature Skill","content":"---\nname: add-feature\ndescription: Add a vertical-slice feature (command/query + handler + validator + endpoint) to an existing FSH module. Use when adding an API endpoint or business operation to a module that already exists.\nargument-hint: \"[ModuleName] [Area] [FeatureName]\"\n---\n\n# Add Feature\n\nA feature is a vertical slice **split across two projects**: the request/response types live in the\nmodule's `.Contracts` project (public API); the handler, validator, and endpoint live in the runtime\nproject. Full conventions: `.agents/rules/api-conventions.md`.\n\n## Layout (real)\n\n```\nsrc/Modules/{X}/Modules.{X}.Contracts/v1/{Area}/{Feature}Command.cs   # ICommand<T>/IQuery<T>\nsrc/Modules/{X}/Modules.{X}.Contracts/Dtos/{Entity}Dto.cs             # response DTOs (if any)\nsrc/Modules/{X}/Modules.{X}/Features/v1/{Area}/{Feature}/\n├── {Feature}CommandHandler.cs    # public sealed, injects the DbContext directly\n├── {Feature}CommandValidator.cs  # required for commands + paginated queries\n└── {Feature}Endpoint.cs          # internal static extension\n```\n\n## Step 1 — Command/Query (Contracts project)\n\n`Mediator` interfaces (`using Mediator;`). Records. A create command can return the raw `Guid`.\n\n```csharp\nnamespace FSH.Modules.{X}.Contracts.v1.{Area};\n\npublic sealed record Create{Entity}Command(string Name, decimal PriceAmount, string PriceCurrency)\n    : ICommand<Guid>;\n```\n\nRead/list DTOs go in `Modules.{X}.Contracts/Dtos/`. Paginated queries return `PagedResponse<T>`\n(`FSH.Framework.Shared.Persistence`) — see `query-patterns`.\n\n## Step 2 — Handler (runtime `Features/`) — inject the DbContext, NOT a repository\n\nThere is **no generic `IRepository<T>`**. Inject the module's `{X}DbContext`. `public sealed`, primary\nctor, `ValueTask<T>`, `.ConfigureAwait(false)`, guard first. Tenant/audit fields are auto-stamped — only\ninject `ICurrentUser` if you need the acting user (`GetUserId()` / `GetTenant()`).\n\n```csharp\npublic sealed class Create{Entity}CommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<Create{Entity}Command, Guid>\n{\n    public async ValueTask<Guid> Handle(Create{Entity}Command command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var entity = {Entity}.Create(command.Name, new Money(command.PriceAmount, command.PriceCurrency));\n        dbContext.{Entities}.Add(entity);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return entity.Id;\n    }\n}\n```\n\nThrow `NotFoundException` / `CustomException(msg, errors, HttpStatusCode)` (`FSH.Framework.Core.Exceptions`) — the global handler maps them to ProblemDetails.\n\n## Step 3 — Validator (required; same folder)\n\n```csharp\npublic sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>\n{\n    public Create{Entity}CommandValidator()\n    {\n        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);\n        RuleFor(x => x.PriceCurrency).NotEmpty().Length(3);\n    }\n}\n```\n\n`Architecture.Tests` fails the build if a command/paginated-query handler has no `{Name}Validator`.\n\n## Step 4 — Endpoint (same folder)\n\n```csharp\npublic static class Create{Entity}Endpoint\n{\n    internal static RouteHandlerBuilder MapCreate{Entity}Endpoint(this IEndpointRouteBuilder endpoints) =>\n        endpoints.MapPost(\"/{entities}\",\n                async (Create{Entity}Command command, IMediator mediator, CancellationToken ct) =>\n                    Results.Ok(await mediator.Send(command, ct)))\n            .WithName(\"Create{Entity}\")\n            .WithSummary(\"Create a {entity}\")\n            .RequirePermission({X}Permissions.{Entities}.Create)\n            .WithIdempotency();   // on replay-safe POSTs\n}\n```\n\n## Step 5 — Wire it in `{X}Module.MapEndpoints`\n\n```csharp\ngroup.MapCreate{Entity}Endpoint();   // group = endpoints.MapGroup(\"api/v{version:apiVersion}/{x}\") …\n```\n\n## Step 6 — Verify\n\n```bash\ndotnet build src/FSH.Starter.slnx          # 0 warnings (TreatWarningsAsErrors)\ndotnet test src/Tests/{X}.Tests            # + add a handler/validator test (see testing-guide)\n```\n\n## Checklist\n\n- [ ] Command/Query in the **Contracts** project (`using Mediator;`), DTOs in `Contracts/Dtos/`\n- [ ] Handler `public sealed`, injects `{X}DbContext` (no repository), `ValueTask<T>` + `.ConfigureAwait(false)`\n- [ ] `{Name}Validator` exists\n- [ ] Endpoint `internal static …Map{Feature}Endpoint`, `.RequirePermission(...)`, `.WithName/.WithSummary`\n- [ ] Wired in `{X}Module.MapEndpoints`\n- [ ] Build 0 warnings; test added\n","category":".agents","tokens":1132}]}