## 1. Project Overview & Quickstart (fullstackhero/CONTRIBUTING)

# CONTRIBUTING

Open-source repository fullstackhero/CONTRIBUTING

### Repository Details
- **Repository:** [fullstackhero/CONTRIBUTING](https://github.com/fullstackhero/CONTRIBUTING)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (fullstackhero/docs)

## File: README.md

# FullStackHero Docs

Documentation site for the FullStackHero .NET starter kit. Built with Astro 6 and Tailwind 4.

## Local development

```bash
cd docs
npm install
npm run dev
```

The site runs on `http://localhost:4321`. Edit MDX files under `src/content/docs/` and the dev server hot-reloads. Note that pagefind search is built only by `npm run build`, so the search modal will report "index not available" during `dev`.

## Build

```bash
npm run build      # astro build && pagefind --site dist
npm run preview    # serves dist/ on a local port
```

## Deploy

Cloudflare Pages, configured by `wrangler.toml`. Production branch: `main`. Build command for the Pages dashboard:

```
npm ci && npm run build
```

Output directory: `dist`.

## Content authoring

Add a new doc page by creating an MDX file under `src/content/docs/<section>/`. Frontmatter shape is defined by `src/content.config.ts`:

```yaml
---
title: Page title
sidebar:
  label: Optional override of title in sidebar
  order: 10                    # lower = higher in section
  hidden: false                # exclude from sidebar but still routable
pageType: guide                # guide | reference | concept | recipe
---
```

The sidebar tree is derived from file structure. Section labels and order live in `src/content/docs/_sections.ts`.

## Design system

Tokens, prose styles, and code-block themes are forked verbatim from `codewithmukesh/blog` (see provenance comments in `src/styles/*.css`). Brand primary is `#15803d` (green-700); the user-facing brand green `#16a34a` lives in `--primary-soft` so it surfaces in accent text, tinted backgrounds, and the gradient mid-stop.

## Search

[Pagefind](https://pagefind.app/). Index is generated as a post-build step over the `dist/` HTML. Only `/docs/*` pages are indexed (scoped via `data-pagefind-body` on the docs `<article>`). Trigger via Ctrl/Cmd+K.

---

## File: docs/specs/2026-05-24-og-image-generator-design.md

# OG Image Generator - Design

**Date:** 2026-05-24
**Status:** Approved, ready for implementation plan

## Goal

Generate per-page Open Graph / Twitter share images (1200×675) for the FullStackHero
documentation site, replacing the current fallback to the 512² square logo
(`/logo-fullstackhero.png`). Adapt the Playwright-based renderer from
`codewithmukesh/blog` (`scripts/og/generate.mjs`), but reshape it from the blog's
**one-bespoke-HTML-per-page** model into a **data-driven generator** that produces a card
for every one of the ~86 docs pages from frontmatter, plus the marketing landing and 404.

## Why the blog's approach can't be copied verbatim

The blog hand-crafts one HTML template per article. With 86 docs pages that doesn't scale.
This repo, however, already has the SEO plumbing the generator can plug into:

- `seo.ogImage` per-page frontmatter (`src/content.config.ts`).
- A site fallback `ogImage: '/logo-fullstackhero.png'` (`src/data/site.ts`).
- `BaseLayout.astro` absolutizes `image ?? siteConfig.ogImage` into `og:image` / `twitter:image`.
- `src/pages/docs/[...slug].astro` reads `seo.ogImage` and passes it down.
- `MarketingLayout.astro` already forwards an `image` prop to `BaseLayout`.

The site also shares the blog's exact fonts (Outfit / Figtree / JetBrains Mono) and dark
background (`#0d0e11`), so the blog's CSS ports over directly - only the accent color changes
from purple to FSH green (`--primary: #15803d`, `--primary-soft: #16a34a`).

## Decisions (from brainstorming)

- **Hybrid template model:** a data-driven default template per page, with an optional bespoke
  per-page HTML override as an escape hatch for hero pages.
- **FSH green, no portrait:** adapt the blog's v2 layout; right column shows the FSH triangle
  mark instead of a face portrait.
- **Auto-derive by slug + override:** images are wired by slug convention; `seo.ogImage` still wins.
- **Four template variants** (see below).
- **pageType drives accent + badge only** within the standard doc template (not separate layouts).
- **Commit the generated JPEGs** under `public/og/` so deploys ship them without running
  Playwright in CI (the build just copies `public/`).

## Template variants

The generator selects a variant automatically:

| Variant        | Applies to                                   | Look                                                                                  |
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------- |
| `home`         | `/docs/` (root `index.mdx`)                  | Hero: large "FullStackHero" wordmark + tagline + prominent FSH logo                   |
| `overview`     | every `<section>/index.mdx`                  | Section name as headline + "Section Overview" eyebrow + "N pages" badge + description |
| `doc`          | leaf docs pages (~60)                        | Section eyebrow + pageType badge & accent + title + description + FSH mark             |
| site templates | `/` (landing) and `404`                      | Bespoke HTML files (blog-style): marketing card / centered 404 card                   |

**pageType accent + badge** (within the `doc` variant):

| pageType    | Accent           | Badge       |
| ----------- | ---------------- | ----------- |
| `guide`     | green (primary)  | GUIDE       |
| `reference` | blue             | REFERENCE   |
| `concept`   | violet           | CONCEPT     |
| `recipe`    | amber            | RECIPE      |

## Architecture

### Components

1. **`scripts/og/template.mjs`** - pure rendering module.
   - Exports `renderDocsTemplate({ variant, eyebrow, title, description, badge, accent })`
     returning a full HTML document string.
   - Holds the shared CSS (FSH-green port of the blog's `_v2-shared.css`).
   - Inlines the FSH logo as a base64 data URI so Playwright `setContent` needs no file paths.
   - Title auto-sizes by length tier; description truncates to ~150 chars.
   - No I/O, no Playwright - independently testable by inspecting returned HTML.

2. **`scripts/og/templates/landing.html`, `scripts/og/templates/404.html`** - bespoke site cards
   (self-contained HTML referencing the shared styles), rendered to `public/og/<name>.jpg`.

3. **`scripts/og/templates/docs/<slug>.html`** - optional per-page override. If a file exists for
   a slug, the generator renders it instead of the data-driven `doc` template. Directory starts empty.

4. **`scripts/og/generate.mjs`** - orchestrator (adapted from the blog).
   - Globs `src/content/docs/**/*.mdx`, parses frontmatter with `gray-matter`.
   - Determines the slug and variant per page (see slug mapping).
   - Resolves section label from `src/content/docs/_sections.ts` for the eyebrow.
   - For each page: if a bespoke override HTML exists, render it; else build HTML via
     `renderDocsTemplate` and render via `page.setContent`.
   - Also renders the named site templates (`landing`, `404`).
   - Screenshots at 1200×675, deviceScaleFactor 2, JPEG quality 88 (identical to the blog).
   - Waits for `networkidle` + `document.fonts.ready` before screenshotting.

### Slug → output mapping (mirrors `[...slug].astro`)

The docs slug is `entry.id` minus `.mdx`, with `/index` stripped and root `index` → empty.
Output path is `public/og/docs/<slug>.jpg`, with the root using `index`:

- root `index.mdx` → `public/og/docs/index.jpg` (variant `home`)
- `architecture/index.mdx` → `public/og/docs/architecture.jpg` (variant `overview`)
- `architecture/vertical-slice.mdx` → `public/og/docs/architecture/vertical-slice.jpg` (variant `doc`)

Variant selection:
- normalized id `=== 'index'` → `home`
- normalized id ends with `/index` → `overview`
- otherwise → `doc`

Eyebrow text: section label from `_sections.ts` (e.g. "Architecture"); root/sectionless → "FullStackHero".

### CLI

```
node scripts/og/generate.mjs                                   # render everything
node scripts/og/generate.mjs docs/architecture/vertical-slice  # one docs page (by slug)
node scripts/og/generate.mjs landing                           # one named site template
```

`npm run og` maps to the no-arg form.

## Wiring

- **`src/pages/docs/[...slug].astro`** - change `const image = seo.ogImage;` to
  `const image = seo.ogImage ?? `/og/docs/${slugForCrumb || 'index'}.jpg`;`
- **`src/pages/index.astro`** - `<MarketingLayout image="/og/landing.jpg">`
- **`src/pages/404.astro`** - add `image="/og/404.jpg"` to its `<MarketingLayout>`.

`BaseLayout` already turns these relative paths into absolute `og:image` / `twitter:image` URLs.
`seo.ogImage` frontmatter continues to override per page.

## package.json

- devDependencies: `playwright`, `gray-matter`.
- script: `"og": "node scripts/og/generate.mjs"`.
- Chromium provisioned once via `npx playwright install chromium`.

## Output & version control

- Generated JPEGs live under `public/og/` (`public/og/docs/*.jpg`, `public/og/landing.jpg`,
  `public/og/404.jpg`) and are committed. `npm run build` copies `public/` into `dist/`.

## Verification

1. Render one page of each variant (`home`, `overview`, `doc`, plus `landing`/`404`); inspect inline.
2. Iterate on the templates until the cards look right.
3. Render all pages.
4. `npm run check` - type + content-schema validation passes.
5. `npm run build` - confirms images are emitted into `dist/og/**` and pages reference the right paths.

## Out of scope

- Running Playwright during CI/build (images are committed instead).
- Per-page bespoke overrides beyond the empty `templates/docs/` escape hatch.
- OG images for any future non-docs routes not listed above.

---

## File: src/content/docs/ai-development/agents-md.mdx

---
title: AGENTS.md & the .agents Folder
lastUpdated: 2026-06-11
sidebar:
  label: AGENTS.md & .agents/
  order: 1
pageType: reference
---

## AGENTS.md - the canonical guide

**AGENTS.md** at the repo root is the single source of truth for AI tools. It's deliberately lean: a
repo map, the tech stack, full-stack build/run commands, a short list of "golden rules" that must never be
broken, and an index pointing into `.agents/rules/`. Conventions are edited *there* - not in the bridges.

**CLAUDE.md** (Claude Code) and **GEMINI.md** (Gemini CLI) are one-line bridges that import it:

```md title="CLAUDE.md"
# Claude Code
The canonical project guide is **`AGENTS.md`** (tool-neutral). It is imported below; edit conventions there, not here.

@AGENTS.md
```

So every tool - and every contributor - reads the same guide, and there's exactly one place to update.

<Callout type="note" title="Why lean?">
Only the root guide loads into the agent's context on every session. Detailed conventions live in
`.agents/rules/` and are pulled in **on demand**, when the agent is actually working in that area. That
keeps each session cheap while still having deep guidance available the moment it's needed.
</Callout>

## The `.agents/` folder

```
AGENTS.md                     ← canonical guide (CLAUDE.md / GEMINI.md import it)
.agents/
├── rules/                    ← conventions & footguns (read on demand)
│   ├── architecture.md          module boundaries, registration, middleware order
│   ├── api-conventions.md       endpoints, CQRS, validation, exceptions, permissions
│   ├── database.md              EF Core, migrations, tenant isolation, query filters
│   ├── eventing.md              domain vs integration events, Outbox/Inbox
│   ├── caching.md  jobs.md  resilience.md  storage.md
│   ├── security.md              CORS, security headers, rate limiting, idempotency, quotas
│   ├── realtime.md  logging.md  testing.md  integration-testing.md
│   ├── buildingblocks-protection.md   read before touching src/BuildingBlocks (it's protected)
│   ├── modules/                 one file per module (identity, multitenancy, chat, files,
│   │                            webhooks, auditing, billing, catalog, tickets, notifications)
│   └── frontend/                shared.md · admin.md · dashboard.md (the two React apps)
├── skills/                   ← task recipes (see Skills & Workflows)
│   └── {skill}/SKILL.md
└── workflows/                ← review / orchestration playbooks
    └── {workflow}.md
```

## Rules - conventions, read on demand

A **rule** is knowledge the agent reads before working in an area - the conventions and the *footguns*
that are easy to get wrong. Rules don't restate what the code or tests already enforce; they point at the
enforcement and capture the non-obvious. The root `AGENTS.md` carries the index, e.g. *"before EF or
migration work, read `database.md`."*

| Working on… | Rule |
|---|---|
| Module structure, boundaries, registration, middleware order | `architecture.md` |
| Endpoints, CQRS, validation, exceptions, permissions | `api-conventions.md` |
| EF Core, entities, migrations, tenant isolation | `database.md` |
| Cross-module events, Outbox/Inbox, idempotent handlers | `eventing.md` |
| Caching · jobs · HTTP resilience · file storage | `caching.md` · `jobs.md` · `resilience.md` · `storage.md` |
| CORS, security headers, rate limiting, idempotency, quotas | `security.md` |
| SignalR / SSE · logging & OpenTelemetry | `realtime.md` · `logging.md` |
| Unit tests · integration tests (Testcontainers) | `testing.md` · `integration-testing.md` |
| Modifying `src/BuildingBlocks` (protected - read first) | `buildingblocks-protection.md` |
| A specific module's quirks | `modules/{module}.md` |
| Any React work · the operator app · the tenant app | `frontend/shared.md` · `frontend/admin.md` · `frontend/dashboard.md` |

A few examples of the kind of footgun these capture: registering a module touches **four** places (Mediator
assemblies + module array, in *both* the API and DbMigrator hosts); tenant isolation is default-on via
`BaseDbContext`; new entity ids use `Guid.CreateVersion7()`; log messages must be structured (no string
interpolation). Each is one read away when the agent needs it.

Next: the **[Skills & Workflows](/docs/ai-development/skills-and-workflows/)** that turn these conventions
into repeatable actions.

---

## File: src/content/docs/ai-development/claude-code.mdx

---
title: Developing with Claude Code
lastUpdated: 2026-05-24
sidebar:
  label: Developing with Claude Code
  order: 3
pageType: guide
seo:
  title: 'Developing fullstackhero with Claude Code'
  keywords: 'Claude Code, .NET, AGENTS.md, AI pair programming, fullstackhero, agent skills, code review workflow'
---

[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is Anthropic's agentic CLI. Because
this repo is wired with `AGENTS.md` and `.agents/`, Claude works *on-convention* out of the box - it
scaffolds slices, wires modules, and writes migrations the way the kit expects.

## 1. Install & open

```bash
npm install @anthropic-ai/claude-code   # see the official docs for native installers
cd dotnet-starter-kit
claude
```

On startup, Claude Code reads `CLAUDE.md`, which imports `AGENTS.md` - so the repo map, golden rules, and
the rules index are in context immediately. The detailed rule files load **on demand** as Claude works.

<Callout type="note" title="One source of truth">
Don't paste conventions into the chat - they're already in `AGENTS.md` and `.agents/rules/`. If something
is wrong or missing, fix the rule file; every session (and every contributor) picks it up.
</Callout>

## 2. Build features by describing them

You don't invoke skills by name - describe the task and the matching skill activates:

| You say… | Skill that runs |
|---|---|
| "Add a `CreateInvoice` feature to Billing" | `add-feature` |
| "Scaffold a `Reviews` module" | `add-module` (incl. the four-place registration) |
| "Add a brands list+create page to the dashboard" | `add-react-page` |
| "Build assignee filtering end-to-end for Tickets" | `add-full-slice` |
| "Create a migration for the new column" | `create-migration` |
| "Publish an `InvoicePaid` event that Notifications reacts to" | `add-integration-event` |

Each skill ends by building/testing, so you get working, on-convention code rather than a sketch.

## 3. Review before you commit

Two read-only workflows act as guardrails - ask for them by intent:

```text
"Review my changes"          → code-reviewer   (diff vs. conventions, structured report)
"Check architecture"         → architecture-guard (boundaries, BuildingBlocks, arch tests)
```

If the Roslyn navigator MCP is configured (the `dotnet-claude-kit` plugin), the reviewer also runs
`detect_antipatterns` and `get_diagnostics` for machine-found issues.

## 4. The build/test loop

```bash
dotnet build src/FSH.Starter.slnx     # 0 warnings - the repo runs TreatWarningsAsErrors
dotnet test src/FSH.Starter.slnx      # unit + architecture + integration
```

<Callout type="warning" title="Integration tests need Docker">
`Integration.Tests` spins up real Postgres, Valkey, and MinIO via Testcontainers. If Docker isn't running
you'll see `DockerUnavailableException` - that's environmental, not a code regression. Run the unit
projects (e.g. `dotnet test src/Tests/Catalog.Tests`) to validate logic without Docker.
</Callout>

To see it all running, the Aspire AppHost brings up the whole stack - Postgres, Valkey, MinIO, the migrator,
the API, and both React apps - with one command:

```bash
dotnet run --project src/Host/FSH.Starter.AppHost
```

## 5. Golden rules worth remembering

Claude follows these from `AGENTS.md`; knowing them helps you review its work:

- **Module boundaries** - modules talk only through `*.Contracts`. Enforced by `Architecture.Tests`.
- **Registering a module touches four places** - Mediator assemblies + module array, in *both* the API and DbMigrator hosts. A missing Mediator marker means handlers silently don't run.
- **Tenant isolation is default-on** via `BaseDbContext`; opt out only via `IGlobalEntity`.
- **Every command + paginated query needs a validator**; handlers are `public sealed`, return `ValueTask<T>`.
- **Structured logging only** - no string interpolation in log messages.
- **Don't modify `src/BuildingBlocks`** without intent - it's shared by every module.

## 6. Keep the guidance sharp

The `.agents/` set is living documentation. When you establish a new convention or hit a new footgun, add
or update a rule (or a skill), commit it, and the whole team's agents improve. The
[AGENTS.md & .agents/](/docs/ai-development/agents-md/) page explains the structure;
[Skills & Workflows](/docs/ai-development/skills-and-workflows/) shows how to extend the set.

---

## File: src/content/docs/ai-development/index.mdx

---
title: Overview
lastUpdated: 2026-05-24
sidebar:
  label: Overview
  order: 0
pageType: concept
seo:
  title: 'AI-Driven Development with fullstackhero'
  keywords: 'AGENTS.md, .agents folder, Claude Code, AI coding agent, agent rules skills workflows, .NET starter kit AI'
---

fullstackhero ships **agent-ready**. The conventions, footguns, and step-by-step recipes that normally
live in a senior engineer's head are written down in a form AI coding tools read on demand - so an agent
scaffolds a feature, adds a module, or writes a migration the *same* way you would, without breaking the
architecture.

Two pieces make this work:

- **AGENTS.md** - the single, tool-neutral guide at the repo root. CLAUDE.md and GEMINI.md are thin bridges that import it, so every tool reads the same source of truth.
- **.agents/** - a folder of **rules** (conventions, read on demand), **skills** (task recipes), and **workflows** (review/orchestration playbooks).

<Callout type="tip" title="Not locked to one tool">
Everything here is plain Markdown keyed off the open [AGENTS.md](https://agents.md) convention.
It works with Claude Code, Gemini CLI, Cursor, Codex, and anything else that reads `AGENTS.md` - no
vendor lock-in.
</Callout>

<SectionIndex section="ai-development" />

---

## File: src/content/docs/ai-development/skills-and-workflows.mdx

---
title: Skills & Workflows
lastUpdated: 2026-05-24
sidebar:
  label: Skills & Workflows
  order: 2
pageType: reference
---

Rules are knowledge; **skills** and **workflows** are action.

- A **skill** (`.agents/skills/{name}/SKILL.md`) is a *verb* - a repeatable, footgun-aware recipe for one task that ends in a green build/test. The agent reaches for it when your request matches its description.
- A **workflow** (`.agents/workflows/{name}.md`) is *orchestration* - it sequences skills and adds review/verification. Workflows delegate the code recipe to the skills rather than duplicating them.

## Skills

### Scaffolders - go from idea to working code

| Skill | What it does |
|---|---|
| `add-feature` | A backend vertical slice: Command/Query in Contracts → handler (injects the module `DbContext`) → validator → endpoint → wired in the module. |
| `add-entity` | A domain aggregate + EF configuration + migration, with the soft-delete / tenant / value-generation conventions baked in. |
| `add-module` | A whole new bounded context - both projects, `IModule`, `BaseDbContext`, permissions, and registration in **all four** sites. |
| `add-react-page` | A frontend slice (admin or dashboard): API module, page, lazy route, permission gate, Playwright test. |
| `add-full-slice` | End-to-end: the backend slice **and** the React page wired to it - the kit's accelerator. |

### Ops - footgun-heavy operations

| Skill | What it does |
|---|---|
| `create-migration` | The exact EF Core migration command (`--project`/`--startup-project`/`--context`/`--output-dir`), build-first to protect the snapshot, review the SQL, apply via the DbMigrator. |
| `add-integration-event` | Publish a cross-module event through the **Outbox** and handle it idempotently - including background tenant-context restoration. |
| `add-permission` | A new permission end-to-end: server constant + endpoint gate, and (admin app) the mirrored catalog entry + route guard. |

### Reference - guardrails

| Skill | What it covers |
|---|---|
| `query-patterns` | Read queries the FSH way - paginated `PagedResponse<T>` via DbContext LINQ; when to use a Specification. |
| `testing-guide` | Test conventions - xUnit + Shouldly + NSubstitute + AutoFixture, naming, and the integration-test gotchas. |
| `mediator-reference` | This kit uses the **Mediator** source generator, *not* MediatR - the interface map and the registration sites. |

## Workflows

| Workflow | Role |
|---|---|
| `feature-scaffolder` | Orchestrates feature delivery - sequences `add-entity` → `add-feature` → `add-react-page`/`add-full-slice` → tests → verify. |
| `module-creator` | Orchestrates a full module bring-up and verifies it actually *loads* (the four-place registration footgun). |
| `migration-helper` | The facts + troubleshooting around EF migrations; delegates the recipe to `create-migration`. |
| `code-reviewer` | Reviews the current diff against the conventions and emits a structured report (read-only). |
| `architecture-guard` | Verifies architectural integrity - module boundaries, BuildingBlocks protection, the architecture-test suite (read-only). |

<Callout type="tip" title="Using them in Claude Code">
You don't memorize skill names. Describe the task - *"add a CreateInvoice feature to Billing"* - and the
matching skill activates automatically. See
[Developing with Claude Code](/docs/ai-development/claude-code/).
</Callout>

## Extending the set

These are plain Markdown - add your own. A good **skill** is a verb (a repeatable task that can self-verify
with a build/test); a good **rule** is a noun (a convention you read). If you find yourself writing a
"reference" or "guide" skill, it's probably a rule. Keep skills pointing *at* the rules rather than
restating them, so there's a single source of truth.

---

## File: src/content/docs/architecture/dependency-injection.mdx

---
title: Dependency injection & module loading
lastUpdated: 2026-06-11
sidebar:
  label: Dependency Injection
  order: 5
pageType: concept
seo:
  title: 'Dependency injection in .NET 10 - Module loader + Mediator'
  keywords: '.net dependency injection patterns, IModule asp.net core, mediator source generator dotnet, fluentvalidation auto register, addheroplatform composition root'
---

fullstackhero's composition root is `src/Host/FSH.Starter.Api/Program.cs`, and it's short. Condensed:

```csharp
// src/Host/FSH.Starter.Api/Program.cs (condensed)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMediator(o =>
{
    o.ServiceLifetime = ServiceLifetime.Scoped;
    o.Assemblies = [
        typeof(GenerateTokenCommand), typeof(GenerateTokenCommandHandler),   // Identity - Contracts + runtime
        typeof(GetTenantStatusQuery), typeof(GetTenantStatusQueryHandler),   // Multitenancy - Contracts + runtime
        /* …two marker types per module, for all ten modules… */ ];
});

var moduleAssemblies = new Assembly[]
{
    typeof(IdentityModule).Assembly,
    typeof(MultitenancyModule).Assembly,
    /* …all ten module runtime assemblies… */
};

builder.AddHeroPlatform(o => { o.EnableCaching = true; o.EnableJobs = true; /* … */ });
builder.AddModules(moduleAssemblies);

var app = builder.Build();
app.UseHeroMultiTenantDatabases();   // Finbuckle UseMultiTenant() - before the rest of the pipeline
app.UseHeroPlatform(p => { p.MapModules = true; /* … */ });
await app.RunAsync();
```

That's the whole composition root. Everything underneath is the platform's job. This page is about what those calls actually do, the rules they impose, and the specific wiring failure that catches every team at least once.

## The four calls

1. **`builder.Services.AddMediator(o => ...)`** - registers Mediator 3's source-generated dispatch. `o.Assemblies` is a list of *marker types*: the generator scans the assembly each type lives in for `ICommand<T>` / `IQuery<T>` / handler implementations. Every module contributes **two** markers - one type from its `.Contracts` assembly and one from its runtime assembly. Handlers are registered with `ServiceLifetime.Scoped`.

2. **`builder.AddHeroPlatform(o => ...)`** - registers the cross-cutting platform: logging, OpenTelemetry, OpenAPI, CORS, security headers, API versioning, exception handling, the validation pipeline behavior, and any optional sub-blocks (caching, jobs, mailing, feature flags, idempotency, SSE, realtime, quotas) you opted into.

3. **`builder.AddModules(moduleAssemblies)`** - runs the `ModuleLoader`. It registers every FluentValidation validator in the supplied assemblies, discovers the assembly-level `[FshModule]` attributes, sorts modules by order, and invokes each one's `ConfigureServices(IHostApplicationBuilder)`.

4. **`app.UseHeroPlatform(p => ...)`** - wires the middleware pipeline in the right order, invoking each module's `ConfigureMiddleware` (right after authentication) and `MapEndpoints` (after authorization) in module order.

## The IModule contract

Every module implements `IModule` and declares itself with an **assembly-level** `[FshModule]` attribute (positional: module type, then order):

```csharp
// BuildingBlocks/Web/Modules/IModule.cs
public interface IModule
{
    void ConfigureServices(IHostApplicationBuilder builder);
    void MapEndpoints(IEndpointRouteBuilder endpoints);
    void ConfigureMiddleware(IApplicationBuilder app) { }   // optional - default no-op
}
```

```csharp
// Modules.Identity/AssemblyInfo.cs
[assembly: FshModule(typeof(FSH.Modules.Identity.IdentityModule), 100)]
```

```csharp
// Modules.Identity/IdentityModule.cs (condensed)
public class IdentityModule : IModule
{
    public void ConfigureServices(IHostApplicationBuilder builder)
    {
        PermissionConstants.Register(IdentityPermissions.All);
        builder.Services.AddScoped();
        builder.Services.AddHeroDbContext();
        // ...
    }

    public void MapEndpoints(IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("api/v{version:apiVersion}/identity")
                             .WithTags("Identity")
                             .WithApiVersionSet(apiVersionSet);
        group.MapGenerateTokenEndpoint().AllowAnonymous().RequireRateLimiting("auth");
        group.MapRegisterUserEndpoint();
        // ... the rest of the module's endpoints
    }
}
```

The `ModuleLoader` runs `ConfigureServices` for **every** module in order, then later runs `ConfigureMiddleware` and `MapEndpoints` for every module in the same order. This two-phase wiring means a module can resolve dependencies registered by an earlier-order module during its own service registration.

## Mediator 3 - source-generated dispatch

Mediator 3 (the `martinothamar/Mediator` library, not to be confused with MediatR) is source-generated. At compile time, its generator walks `MediatorOptions.Assemblies`, finds every `ICommandHandler` and `IQueryHandler`, and emits a static dispatch table. **Zero runtime reflection.**

The catch: the generator only scans assemblies that appear in `o.Assemblies`. A module's commands live in its `.Contracts` assembly and its handlers live in its runtime assembly - so each module needs **both** markers in the list. Leave one out and nothing breaks at build time; the handler is just missing from the dispatch table, and `mediator.Send(...)` fails at runtime. The registration sites and the two-marker rule are documented in `.agents/rules/architecture.md` and the `mediator-reference` skill.

## FluentValidation auto-registration

`ModuleLoader.AddModules` calls `services.AddValidatorsFromAssemblies(moduleAssemblies)`, so every `AbstractValidator<TCommand>` in any module is registered automatically. The Mediator pipeline behavior `ValidationBehavior` (registered by `AddHeroPlatform`) runs them before every command handler.

What this means: drop a `MyCommandValidator : AbstractValidator<MyCommand>` next to the handler, and validation runs on every `mediator.Send(new MyCommand(...))`. No registration line in your module's `ConfigureServices`.

## Three lifetimes, applied consistently

The kit uses standard `IServiceCollection` lifetimes:

| Lifetime | Use for | Example |
|---|---|---|
| Singleton | Stateless / app-wide / thread-safe | `IEventSerializer`, `ITenantInitialPasswordBuffer`, metrics/meter types |
| Scoped | Per-request DbContext, per-request user, Mediator handlers | `DbContext`, `ICurrentUser`, `ITokenService`, `IQuotaService`, command/query handlers (`o.ServiceLifetime = ServiceLifetime.Scoped`) |
| Transient | Stateless services that aren't expensive to construct | `IUserService` and the focused user sub-services, `IConnectionStringValidator` |

Two things to watch out for:

- **Hangfire jobs get a new scope per execution.** `FshJobActivator` creates an `IServiceScope` per job. So a scoped service injected into a job sees a fresh instance for each run. Don't try to share state via scoped services across job invocations; use Valkey or the database.
- **Integration event handlers run in their own scope, not the publisher's.** Handlers publish to the **outbox** (same transaction as the business write); `OutboxDispatcherHostedService` picks the row up asynchronously and the event bus creates a *fresh* DI scope per event before resolving handlers. Don't expect to share a DbContext or transaction with the publisher - idempotency comes from the Inbox (`{EventId, HandlerName}` dedupe), not shared state.

## Permission registration

`PermissionConstants` is a static registry. Each module calls it during `ConfigureServices`:

```csharp
PermissionConstants.Register(IdentityPermissions.All);
PermissionConstants.Register(CatalogPermissions.All);
PermissionConstants.Register(TicketsPermissions.All);
// ...
```

The registration is global, deduped by `Name`, and additive. There's no removal API - once registered, a permission is part of the runtime for the process lifetime.

The Identity module's `RequiredPermissionAuthorizationHandler` reads the permission from the endpoint metadata and checks it against the caller's claims. The kit's `RequiredPermissionAttribute` and `.RequirePermission()` fluent helper both produce that same metadata.

<Callout type="warning" title="Watch for duplicate RequiredPermissionAttribute types">
`RequiredPermissionAttribute` (in the Shared block) implements `IRequiredPermissionMetadata` - that interface is what the authorization handler looks up on the endpoint. If a copy of the attribute lands in another assembly **without implementing the interface**, every `.RequirePermission()` call silently stops gating. This is a runtime no-op, not a build break - and it's the single nastiest failure mode in the kit's auth pipeline. The handler carries a source comment warning about exactly this.
</Callout>

## The four wire points when you add a new module

Adding a new module touches four lists across **two host files**. Miss any one of them and it fails *silently*:

| # | Place | File | Symptom if missed |
|---|---|---|---|
| 1 | Mediator `o.Assemblies` - two markers (a Contracts type **and** the module type) | `src/Host/FSH.Starter.Api/Program.cs` | Handlers silently undiscovered |
| 2 | `moduleAssemblies` array | `src/Host/FSH.Starter.Api/Program.cs` | Module never loaded |
| 3 | Mediator assemblies (same pair) | `src/Host/FSH.Starter.DbMigrator/Program.cs` | Migrate/seed misses the module |
| 4 | Module assemblies array | `src/Host/FSH.Starter.DbMigrator/Program.cs` | Migrate/seed misses the module |

The DbMigrator pair is the one everyone forgets: it mirrors the API's registration because some module DbInitializers depend on services the Mediator pipeline builds. Forgetting #1 is the nastiest failure mode - the host boots, the module's services register, the endpoints map, but `mediator.Send(new MyCommand())` fails at runtime because the dispatch table doesn't include the handler. (You'll also add the new `*.Contracts` and runtime projects to `FSH.Starter.slnx`, but the compiler catches that one for you.)

## The middleware pipeline order

`app.UseHeroMultiTenantDatabases()` runs first, in `Program.cs` before `UseHeroPlatform` - it's Finbuckle's `UseMultiTenant()`, so tenant resolution happens before everything below (including authentication). Then `UseHeroPlatform` wires the pipeline in a deliberate order:

```
1.  UseExceptionHandler            → RFC 9457 ProblemDetails
2.  UseResponseCompression
3.  UseCors                        ← before HTTPS redirect (preflight)
4.  UseHttpsRedirection
5.  Security headers
6.  UseStaticFiles                 (optional)
7.  Hangfire dashboard             (if jobs enabled)
8.  UseRouting
9.  OpenAPI + Scalar
10. UseAuthentication
11. Per-module ConfigureMiddleware ← multi-tenant root override etc.
12. UseRateLimiter
13. Quota enforcement              (if quotas enabled)
14. UseAuthorization
15. Per-module MapEndpoints
16. Health, SSE, SignalR
17. CurrentUserMiddleware          ← last, so authorization already done
```

Three ordering rules are unusual and important:

- **Tenant resolution before authentication.** Finbuckle's strategy chain sees an anonymous `User`, which is why claim-aware tenant logic lives in post-auth module middleware - see the [multitenancy deep dive](/docs/architecture/multitenancy-deep-dive/).
- **CORS before HTTPS redirect.** Preflight `OPTIONS` requests can't follow redirects per the Fetch spec.
- **Per-module middleware after authentication.** `UseModuleMiddlewares` runs each module's `ConfigureMiddleware` right after `UseAuthentication`, so module middleware (like Multitenancy's root-operator override) can read claims.

`CurrentUserMiddleware` runs last: it populates `ICurrentUser` from claims for the endpoint to consume. Anything reading `ICurrentUser` earlier in the chain sees nothing - middleware that needs the user reads claims directly.

## Related

- [Web building block](/docs/building-blocks/web/) - the composition + middleware code.
- [Vertical Slice](/docs/architecture/vertical-slice/) - what a handler looks like.
- [Modular monolith](/docs/architecture/modular-monolith/) - the module ordering rules.

---

## File: src/content/docs/architecture/index.mdx

---
title: Overview
lastUpdated: 2026-05-19
sidebar:
  order: 1
pageType: concept
seo:
  title: 'Architecture - modular monolith + vertical slice in .NET 10'
---

How fullstackhero is put together - the modular monolith, vertical slice architecture, and how modules talk to each other through contracts.

<SectionIndex section="architecture" />

---

## File: src/content/docs/architecture/modular-monolith.mdx

---
title: Modular monolith
lastUpdated: 2026-06-11
sidebar:
  label: Modular monolith
  order: 2
pageType: concept
seo:
  title: 'Modular monolith in .NET 10 - module boundaries enforced by'
  keywords: 'modular monolith .net 10, .net module boundaries, netarchtest enforced architecture, modular monolith to microservices migration'
---

fullstackhero is a **modular monolith**. Ten modules - Identity, Multitenancy, Auditing, Files, Chat, Notifications, Webhooks, Billing, Catalog, Tickets - live in one repository, build one set of containers, and deploy as one process. Each is a bounded context with its own `DbContext`, its own feature folders, and a single one-way dependency channel (its `*.Contracts` assembly) through which the rest of the kit talks to it.

<Callout type="tip" title="It's not a step on the way to microservices">
A modular monolith is its own architecture. You ship one process, one deploy, one set of logs, one set of traces - which is faster than running ten services for most teams. When a module hits a true horizontal-scaling need, the boundaries you've already drawn make extraction a refactor, not a rewrite.
</Callout>

## The shape

<FolderTree
  root="src/"
  caption="One repo · one process"
  tree={[
    {
      name: 'BuildingBlocks/',
      note: 'Shared infrastructure - 11 libraries',
      chips: [
        'Core',
        'Persistence',
        'Web',
        'Shared',
        'Caching',
        'Eventing',
        'Eventing.Abstractions',
        'Jobs',
        'Mailing',
        'Storage',
        'Quota',
      ],
    },
    {
      name: 'Modules/',
      note: 'Ten bounded contexts',
      children: [
        {
          name: 'Identity/',
          children: [
            { name: 'Modules.Identity/', note: 'Runtime - endpoints, handlers, domain' },
            {
              name: 'Modules.Identity.Contracts/',
              note: 'Public surface - commands, queries, DTOs, events',
              highlight: true,
            },
          ],
        },
        { name: 'Multitenancy/', badge: '+ Contracts' },
        { name: 'Auditing/', badge: '+ Contracts' },
        { name: 'Files/', badge: '+ Contracts' },
        { name: 'Chat/', badge: '+ Contracts' },
        { name: 'Notifications/', badge: '+ Contracts' },
        { name: 'Webhooks/', badge: '+ Contracts' },
        { name: 'Billing/', badge: '+ Contracts' },
        { name: 'Catalog/', badge: '+ Contracts' },
        { name: 'Tickets/', badge: '+ Contracts' },
      ],
    },
    {
      name: 'Host/',
      note: 'Composition root + orchestrator',
      children: [
        { name: 'FSH.Starter.Api/', note: 'The API process' },
        { name: 'FSH.Starter.AppHost/', note: '.NET Aspire orchestrator' },
        { name: 'FSH.Starter.DbMigrator/', note: 'Migrations + demo seeder CLI' },
        { name: 'FSH.Starter.Migrations.PostgreSQL/' },
      ],
    },
    { name: 'Tools/CLI/', note: 'The fsh CLI (Spectre.Console)' },
  ]}
/>

Every module follows the same shape: a runtime project that does the work, a contracts project that defines its public surface, and (optionally) a migrations project for its EF Core schema.

## The boundary rule

Modules talk to each other **only through `*.Contracts` assemblies**.

| Allowed | Not allowed |
|---|---|
| `Modules.Chat.dll` references `Modules.Notifications.Contracts.dll` | `Modules.Chat.dll` references `Modules.Notifications.dll` |
| `Modules.Identity.dll` references `BuildingBlocks/*` | `Modules.Identity.dll` references any other module's runtime |
| Any module's `Contracts.dll` references `Eventing.Abstractions.dll` | A `Contracts.dll` references `Eventing` (the runtime) or EF Core |

The cardinal rule: **runtime modules never reference each other.** If module A needs something from module B, it asks module B's contracts - which is a thin layer of commands, queries, events, and DTOs.

These rules aren't conventions; they're enforced by `src/Tests/Architecture.Tests/`. The boundary test scans every module runtime `.csproj` for forbidden project references:

```csharp
// Architecture.Tests/ModuleArchitectureTests.cs (condensed)
[Fact]
public void Modules_Should_Not_Depend_On_Other_Modules()
{
    // for every Modules.*.csproj (excluding *.Contracts), assert no ProjectReference
    // points at another module's runtime project
    isSelfReference.ShouldBeTrue(
        $"Module runtime project '{currentName}' must not reference other module runtime project '{referencedName}'. " +
        "Only contracts or building block projects are allowed.");
}
```

A second layer of [NetArchTest](https://github.com/BenMorris/NetArchTest) tests guards the finer rules - Contracts assemblies can't depend on EF Core, FluentValidation, or Hangfire; domain types can't reach into persistence. Break a rule and the test suite fails.

## How modules talk to each other

Three patterns, all one-way and contract-only:

### 1. Request a service (rare)

Some contracts expose service interfaces - `ITokenService`, `IUserService` (in `Modules.Identity.Contracts`) - that downstream modules can resolve via DI without referencing the runtime. The interface lives in `*.Contracts`; the implementation lives in the runtime; consumers depend only on the contract. (`ICurrentUser` works the same way, but lives one level down in `BuildingBlocks/Core`; Identity registers the implementation.)

### 2. Domain events (in-module)

Domain events are private to the aggregate's module. The `DomainEventsInterceptor` (from the Persistence block) dispatches them through Mediator after SaveChanges. Handlers live in the same module; cross-module dependency is zero.

### 3. Integration events (cross-module)

The canonical pattern. A module writes an `IIntegrationEvent` to the **outbox** (`IOutboxStore.AddAsync`, committed with the business write); the kit's outbox dispatcher publishes it asynchronously via `IEventBus`; any module that implements `IIntegrationEventHandler<TEvent>` receives a copy, with inbox-backed idempotency. Examples in the kit:

- **Chat → Notifications**: `MentionedInChannelIntegrationEvent` → inbox row + SignalR push.
- **Any module → Webhooks**: open-generic `WebhookFanoutHandler<TEvent>` fans every event to subscribed tenants.
- **Identity → outbox**: `UserRegisteredIntegrationEvent`, `TokenGeneratedIntegrationEvent` for downstream consumers.

The receiver doesn't know the producer. The producer doesn't know the receivers. That's the discipline.

## Module load order

Each module runtime carries an assembly-level attribute - `[assembly: FshModule(typeof(IdentityModule), 100)]`. Lower numbers run their `ConfigureServices` and `MapEndpoints` first. The ordering matters for two specific cases:

- **Cross-module integration-event handlers.** Notifications (order `750`) must load before Chat (order `800`) because Notifications registers handlers for Chat's `MentionedInChannelIntegrationEvent`. If Chat loaded first, the handler wouldn't be wired when the first mention event fires.
- **`ICurrentUser` availability.** Identity (order `100`) loads first so every later module sees the JWT pipeline and `ICurrentUser` registered.

| Order | Module |
|---|---|
| 100 | Identity |
| 200 | Multitenancy |
| 300 | Auditing |
| 350 | Files |
| 400 | Webhooks |
| 500 | Billing |
| 600 | Catalog |
| 700 | Tickets |
| 750 | Notifications |
| 800 | Chat |

The `ModuleLoader` (in `BuildingBlocks/Web`) runs each module's `ConfigureServices` in order, then later runs `ConfigureMiddleware` and `MapEndpoints` in the same order.

## When a module needs to scale out

The boundaries you've already drawn are the contract for extraction:

1. The module's runtime moves into its own service.
2. Its `*.Contracts` assembly is shared (either as a NuGet package or duplicated).
3. The cross-module communication switches from in-process Mediator (for service calls) and `InMemoryEventBus` (for events) to HTTP / RabbitMQ.
4. Consumers don't change - they still publish via `IEventBus`, still resolve services via DI. Only the transport behind them changes.

The kit ships `RabbitMqEventBus` as the cross-process implementation. Wire it in `EventingOptions:Provider = "RabbitMQ"` and you can run two halves of the monolith as two processes while keeping the surface identical.

You almost certainly don't need this for a long time. The point is that you don't have to commit to it on day one.

## Why not microservices on day one?

Two reasons.

**Operational cost.** Ten services means ten deploys, ten log streams, ten sets of dashboards, ten failure modes when one service is down. A modular monolith collapses that to one - and you get genuine isolation through your code structure, not your network topology.

**Wrong boundaries.** When you're shipping the first version of a SaaS, you don't yet know which boundaries are stable. Drawing service lines now means committing to API contracts that are about to change. A modular monolith lets you redraw boundaries in a refactor, not a coordinated multi-service deploy.

When a module's traffic pattern, scaling needs, or team ownership genuinely diverges from the rest - extract it. Until then, keep them on one box.

## Related

- [Vertical Slice Architecture](/docs/architecture/vertical-slice/) - what each module looks like internally.
- [Modules overview](/docs/modules/) - the ten that ship in v10.
- [Eventing.Abstractions](/docs/building-blocks/eventing-abstractions/) - the contract for cross-module talk.
- [Web building block](/docs/building-blocks/web/) - the module loader.