This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Never add "Co-authored-by: Claude" or any Claude/AI attribution to git commits
- Never add comments like "Generated by Claude", "AI-generated", or similar in source code
- Never list Claude as a contributor in any file (package.json, csproj, README, etc.)
- All commits should appear as authored solely by the developer — do not modify git author/committer config
- Do not append the claude.ai session URL to commit messages
Modular monolith framework for .NET with compile-time module discovery via Roslyn source generators. Frontend uses React 19 + Inertia.js served via a static HTML shell.
Do NOT start your own Postgres container. All repos in ~/Repos share one
stack defined in ~/Repos/dev-services (one PostGIS + Redis + MinIO + Adminer on
the devnet Docker network). Start it once with make up there.
- Local dev defaults to SQLite (
app.db), so Postgres is optional here. - To use Postgres, point the app at the shared server:
localhost:5432,postgres/postgres, databasesimplemodule(already created by the shared stack's init SQL). - This repo's own
docker-compose.ymlstarts apostgres:17container (dbsimplemodule, credssimplemodule/simplemodule). Prefer the shared stack and skip it — running both conflicts on port 5432. Running via the Aspire AppHost also provisions its own Postgres (simplemoduledb); that's a separate path from the shared stack.
If 5432 is taken by an old per-project container, stop that container rather than
remapping ports. To add a database, edit dev-services/init/01-databases.sql.
dotnet build
dotnet run --project template/SimpleModule.Host # runs on https://localhost:5001npm install # install all workspace dependencies
npm run dev # start development (dotnet + all module watches, unminified JS)
npm run build:dev # build all modules in dev mode (unminified, with source maps)
npm run build # production build (minified, optimized)
npm run check # biome lint + format check
npm run check:fix # auto-fix lint + formatting
npm run lint # lint only
npm run format # format only (with write)Run npm run dev to start the complete development environment:
npm run dev
# Starts:
# - dotnet run (ASP.NET backend on https://localhost:5001)
# - npm watch for all modules (unminified, with source maps)
# - npm watch for ClientApp (unminified, with source maps)The orchestrator will coordinate all processes:
- Edit a module file → Vite rebuilds (fast, unminified, readable code)
- Edit ClientApp → Vite rebuilds (fast, unminified)
- Browser refresh → See changes immediately
- Browser dev tools → See original TypeScript thanks to source maps
- Ctrl+C → Gracefully stops all processes
- Development (
npm run build:dev) — unminified, source maps enabled, for local iteration - Production (
npm run build) — minified, optimized, for distribution and NuGet packages
Workspaces: modules/*/src/*, packages/*, and template/SimpleModule.Host/ClientApp.
dotnet test # all tests
dotnet test --filter "FullyQualifiedName~ClassName" # single test class
dotnet test --filter "FullyQualifiedName~MethodName" # single test methodTest stack: xUnit.v3, FluentAssertions, Bogus, Microsoft.AspNetCore.Mvc.Testing. SQLite in-memory for unit and integration tests (a PostgreSQL CI leg is a known gap — the test factory is SQLite-only today).
dotnet run -c Release --project tests/SimpleModule.Benchmarks # all benchmarks
dotnet run -c Release --project tests/SimpleModule.Benchmarks -- --filter "*Users*" # specific moduleMicro-benchmarks for Admin, AuditLogs, FileStorage, Settings, and Users: endpoint latency (CRUD operations via in-process TestServer), JSON serialization/deserialization of DTOs. Uses SimpleModuleWebApplicationFactory with test auth headers for low-overhead measurement.
dotnet test tests/SimpleModule.LoadTests # all scenarios
dotnet test tests/SimpleModule.LoadTests --filter "Users_Crud" # single scenarioHTTP load tests using real OAuth Bearer tokens acquired via ROPC (password grant) from OpenIddict. Runs against the full ASP.NET pipeline with file-based SQLite in WAL mode. Six scenarios (tests/SimpleModule.LoadTests/Scenarios/), each runnable individually or combined via All_Scenarios:
- Users (
Users_Crud) — full CRUD lifecycle - Settings (
Settings_Ops) — read operations - AuditLogs (
AuditLogs_Read) — read operations - FileStorage (
Files_Ops) — file operations - Admin (
Admin_Ops) — role create/delete (handles 302 redirects) - FeatureFlags (
FeatureFlags_Ops) — get all flags, check flag status
Key infrastructure:
LoadTestWebApplicationFactory— extendsWebApplicationFactorywith file-based SQLite + WAL, seeds OAuth client/user/permissions, acquires Bearer tokens via/connect/tokenPasswordGrantTokenHandler— OpenIddict event handler for ROPC grant typeSqliteBusyTimeoutInterceptor— setsPRAGMA busy_timeout=30000on every EF Core connection
- SimpleModule.Core —
IModuleinterface,[Module]attribute,IEndpointinterface,[Dto]attribute, menu system (IMenuRegistry), domain event base types (IEvent/DomainEventinSimpleModule.Core.Events, dispatched in-process via WolverineIMessageBus), Inertia integration. - SimpleModule.Generator — Roslyn
IIncrementalGenerator(netstandard2.0). Scans referenced assemblies for[Module]classes,IEndpointimplementors, and[Dto]types. Generates:AddModules(),MapModuleEndpoints(),CollectModuleMenuItems(), JSON serializers, TypeScript interface definitions. - SimpleModule.Host — Host app (net10.0). Calls generated extension methods in
Program.cs. Inertia middleware renders static HTML shell with embedded JSON props for React hydration.
- ClientApp (
template/SimpleModule.Host/ClientApp/app.tsx) — Inertia bootstrap. Resolves pages by splitting route name (e.g.,Tenants/Browse→ imports/_content/Tenants/Tenants.pages.js). - Module pages — Each module builds its React pages via Vite in library mode →
{ModuleName}.pages.jsin module'swwwroot/. Entry point:Pages/index.tsexporting apagesrecord mapping route names to components. - Type generation —
[Dto]types → source generator embeds TS interfaces →scripts/extract-ts-types.mjswrites.tsfiles toClientApp/types/.
- ASP.NET route handler calls
Inertia.Render("Tenants/Browse", props) - Inertia middleware renders static HTML shell with embedded JSON props
- React ClientApp dynamically imports module's
pages.jsbundle - Component hydrates with server-provided props
See docs/CONSTITUTION.md for the authoritative reference on:
- Module boundaries, dependencies, and data ownership
- Communication patterns (contracts and events)
- Endpoint, frontend, and authorization rules
- Compiler-enforced diagnostics (SM0001-SM0061)
- Framework contributor guidelines
- Source generator must target netstandard2.0 with
IIncrementalGenerator(notISourceGenerator). - Modules need
<FrameworkReference Include="Microsoft.AspNetCore.App" />. - Module Vite builds use library mode — externalize React, React-DOM, @inertiajs/react.
- TreatWarningsAsErrors is enabled globally via
Directory.Build.propswithAnalysisLevel=latest-allandAnalysisMode=All. Suppressed rules are listed in.editorconfig.
- Naming: Interfaces
IFoo, public membersPascalCase, private fields_camelCase, locals/paramscamelCase, constantsPascalCase. - Style: File-scoped namespaces (error), usings outside namespace (error), prefer
var. - Tests: Underscore method names allowed (
Method_Scenario_Expected). CA2234 (Uri overload) and xUnit1051 (CancellationToken) suppressed in test projects.
When you add a new IViewEndpoint, you must register it in your module's Pages/index.ts immediately. This is a manual, critical step.
Why: The C# source generator discovers your new endpoint and validates it's properly decorated, but React needs a corresponding entry in the page registry. If you forget:
- The endpoint compiles and runs fine on the server
- Navigating to that page throws a descriptive client-side error — the page resolver names the missing page and lists the available ones, the ClientApp shows an error toast, and the error is logged to the browser console
npm run validate-pagescatches the mismatch at build time (and in CI) before it reaches the browser
Pattern:
// modules/Tenants/src/SimpleModule.Tenants/Pages/index.ts
export const pages: Record<string, unknown> = {
'Tenants/Browse': () => import('./Browse'),
'Tenants/Manage': () => import('./Manage'),
'Tenants/Create': () => import('./Create'),
};The Rule: For every IViewEndpoint with Inertia.Render("Tenants/Something", ...), add a matching entry in pages. The component name in Inertia.Render (e.g., "Tenants/Manage") is your key.
Validation: After adding endpoints, run:
npm run validate-pagesThis script checks that all C# endpoints have corresponding TypeScript entries. If mismatches are found, it logs them and exits with error code 1 (useful for CI).
SimpleModule.Tests.SharedprovidesSimpleModuleWebApplicationFactory— in-memory SQLite, test auth scheme withCreateAuthenticatedClient(params Claim[] claims), claims passed viaX-Test-Claimsheader.FakeDataGenerators(Bogus) — pre-built fakers for all module DTOs and request types.- CI runs tests against both SQLite and PostgreSQL.
- @simplemodule/client — Vite plugin for vendoring, page resolution utility.
- @simplemodule/ui — Radix UI component wrappers with Tailwind. Import components from
@simplemodule/ui/components, utils from@simplemodule/ui/lib/utils. - @simplemodule/theme-default — Tailwind CSS base theme.
sm new project # scaffold new SimpleModule solution
sm new module <name> # create module with contracts, endpoints, tests, events
sm new feature <name> # add feature to existing module
sm doctor [--fix] # validate project structure, auto-fix issuesUse sm new module <name> (CLI) or manually:
- Create
modules/<Name>/ - Create
modules/<Name>/src/SimpleModule.<Name>.Contracts/with:SimpleModule.<Name>.Contracts.csproj(Microsoft.NET.Sdk; referencesframework/SimpleModule.Coreonly)I<Name>Contracts.cs— public interface for cross-module use- Shared DTO types marked with
[Dto]
- Create
modules/<Name>/src/SimpleModule.<Name>/with:SimpleModule.<Name>.csproj(Microsoft.NET.Sdk.StaticWebAssets; references Core + Contracts with<FrameworkReference Include="Microsoft.AspNetCore.App" />; add aSimpleModule.Databasereference only if the module owns a DbContext)<Name>Module.cs— implementsIModulewith[Module("Name", RoutePrefix = "...")]Endpoints/<Feature>/— endpoint classes implementingIEndpoint(auto-discovered)Pages/—IViewEndpointclasses co-located with their React.tsxcomponents (optionally grouped in feature subfolders)Pages/index.ts— exportspagesrecord mapping route names to React componentsvite.config.ts— library mode build targetingPages/index.tspackage.json— declare React/Inertia as peerDependencies- Register contract interface in
ConfigureServices - Escape hatch: For non-standard routes, implement
ConfigureEndpointson the module class
- Create
modules/<Name>/tests/SimpleModule.<Name>.Tests/with xUnit test project - Add
ProjectReferencetotemplate/SimpleModule.Host/SimpleModule.Host.csproj - Add all projects to
SimpleModule.slnx
Biome is configured at repo root (biome.json). Covers modules/**, packages/**, template/** except **/wwwroot/**. Settings: single quotes, semicolons always, 2-space indent, trailing commas, 100-char line width. Tailwind CSS directives enabled.
- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions)
- If something goes sideways, STOP and re-plan immediately - don't keep pushing
- Use plan mode for verification steps, not just building
- Write detailed specs upfront to reduce ambiguity
- Use subagents liberally to keep main context window clean
- Offload research, exploration, and parallel analysis to subagents
- For complex problems, throw more compute at it via subagents
- One task per subagent for focused execution
- After ANY correction from the user: update
tasks/lessons.mdwith the pattern - Write rules for yourself that prevent the same mistake
- Ruthlessly iterate on these lessons until mistake rate drops
- Review lessons at session start for relevant project
- Never mark a task complete without proving it works
- Diff behavior between main and your changes when relevant
- Ask yourself: "Would a staff engineer approve this?"
- Run tests, check logs, demonstrate correctness
- For non-trivial changes: pause and ask "is there a more elegant way?"
- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution"
- Skip this for simple, obvious fixes - don't over-engineer
- Challenge your own work before presenting it
- When given a bug report: just fix it. Don't ask for hand-holding
- Point at logs, errors, failing tests - then resolve them
- Zero context switching required from the user
- Go fix failing CI tests without being told how
- Plan First: Write plan to
tasks/todo.mdwith checkable items - Verify Plan: Check in before starting implementation
- Track Progress: Mark items complete as you go
- Explain Changes: High-level summary at each step
- Document Results: Add review section to
tasks/todo.md - Capture Lessons: Update
tasks/lessons.mdafter corrections
- Simplicity First: Make every change as simple as possible. Impact minimal code
- No Laziness: Find root causes. No temporary fixes. Senior developer standards
- Never add Claude/AI attribution to commits or source code (see Attribution Policy above)