Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
165 commits
Select commit Hold shift + click to select a range
83af320
Eternelle-176 Strengthen code review and DDD enforcement in .coderabb…
engrkwakwak May 24, 2026
35a10a6
Eternelle-174 Add Users module with CQRS, EF Core, and Keycloak auth …
engrkwakwak May 25, 2026
1b819b6
feat(catalog): scaffold five-project module structure
engrkwakwak May 27, 2026
75139b7
feat(catalog): add TemplateTier, PriceClass, SectionId enums
engrkwakwak May 27, 2026
ab649c5
feat(catalog): add strongly-typed ids for Template and children
engrkwakwak May 27, 2026
e88b1bd
feat(catalog): add value objects and errors
engrkwakwak May 27, 2026
e159b63
feat(catalog): add jsonb theme styling records
engrkwakwak May 27, 2026
5567a65
feat(catalog): add Template child entities with variant upload limit
engrkwakwak May 27, 2026
d503809
feat(catalog): add Template domain events and manifest payload
engrkwakwak May 27, 2026
f1ca0ce
feat(catalog): add TemplateErrors
engrkwakwak May 27, 2026
8b245d6
feat(catalog): add Template aggregate root
engrkwakwak May 27, 2026
ba49d6e
feat(catalog): add ITemplateRepository
engrkwakwak May 27, 2026
0eb10cf
style(catalog): remove what-comment from UploadLimit property
engrkwakwak May 27, 2026
724f3fe
Eternelle-180 Add CatalogTemplateManifestChanged integration event
engrkwakwak May 27, 2026
29b8351
Eternelle-180 Add IUnitOfWork and manifest domain-event handler
engrkwakwak May 27, 2026
033c22c
Eternelle-180 Add CreateTemplate command and cache keys
engrkwakwak May 27, 2026
5027920
Eternelle-180 Add UpdateTemplate command
engrkwakwak May 27, 2026
09703e6
Eternelle-180 Add AddTemplateTheme command
engrkwakwak May 27, 2026
8c828d5
Eternelle-180 Add AddSupportedSection command
engrkwakwak May 27, 2026
942a68b
Eternelle-180 Add AddSectionVariant command with upload limit
engrkwakwak May 27, 2026
febdab8
Eternelle-180 Add GetTemplates query
engrkwakwak May 27, 2026
af8134e
Eternelle-180 Add GetTemplateById query
engrkwakwak May 27, 2026
231b6b5
Eternelle-180 Add GetSupportedSections and GetSectionVariants queries
engrkwakwak May 27, 2026
e7f88ee
Eternelle-180 Add CatalogDbContext, id converters, and entity configu…
engrkwakwak May 27, 2026
e4d23ee
Eternelle-180 Add TemplateRepository
engrkwakwak May 27, 2026
bf5d704
Eternelle-180 Add outbox plumbing and CatalogModule DI
engrkwakwak May 27, 2026
70bf5ea
Eternelle-180 Add presentation layer endpoints (Tasks 26-27)
engrkwakwak May 27, 2026
6be8d99
Eternelle-180 Wire Catalog module into the API host and add initial m…
engrkwakwak May 27, 2026
23873b5
Eternelle-180 Seed catalog:manage permission for Admin role
engrkwakwak May 27, 2026
0d94173
Eternelle-180 Apply file-scoped namespace and collection expression f…
engrkwakwak May 27, 2026
7ec9a03
Eternelle-180 Add migration support for CatalogDbContext
engrkwakwak May 27, 2026
fef0171
Eternelle-180 Address code review findings on Template aggregate and …
engrkwakwak May 27, 2026
8169c19
Eternelle-180 Refactor init patterns, migrations, and add comments
engrkwakwak May 27, 2026
0108e90
Merge pull request #181 from engrkwakwak/eternelle-180
engrkwakwak May 27, 2026
aa92630
Fix merge conflict
engrkwakwak May 27, 2026
ca77d6f
Merge pull request #183 from engrkwakwak/hotfix
engrkwakwak May 27, 2026
c5386be
Eternelle-184 Persist Catalog enums as integers; update docs and seed…
engrkwakwak May 27, 2026
00a6ac8
Merge pull request #185 from engrkwakwak/eternelle-184
engrkwakwak May 27, 2026
ae87253
Eternelle-186 Scaffold five-project module structure
engrkwakwak May 28, 2026
6d85457
Eternelle-186 Remove circular Presentation reference from Infrastructure
engrkwakwak May 28, 2026
d9ccd18
Eternelle-186 Add strongly-typed ids
engrkwakwak May 28, 2026
59e1e27
Eternelle-186 Add tenant value objects
engrkwakwak May 28, 2026
1945ebc
Eternelle-186 Add section keys and template section seed
engrkwakwak May 28, 2026
1fe0006
Eternelle-186 Add tenant domain events
engrkwakwak May 28, 2026
00426b6
Eternelle-186 Add TenantSectionEntry owned entity
engrkwakwak May 28, 2026
efcc7d0
Eternelle-186 Add Tenant aggregate, errors, and repository interface
engrkwakwak May 28, 2026
8fefefc
Eternelle-186 Add CatalogTemplate projection model and reader interface
engrkwakwak May 28, 2026
b409e83
Eternelle-186 Add outbound integration events and Application Assembl…
engrkwakwak May 28, 2026
b281e49
Eternelle-186 Publish integration events from domain-event handlers
engrkwakwak May 28, 2026
03b2178
Eternelle-186 Add ProvisionTenant command
engrkwakwak May 28, 2026
abf6e47
Eternelle-186 Add AssignTemplate, ConfigureSection, ReorderSections, …
engrkwakwak May 28, 2026
47b9123
Eternelle-186 Add tenant queries
engrkwakwak May 28, 2026
16f5250
Eternelle-186 Add DbContext, Schemas, and ID converters
engrkwakwak May 28, 2026
9446c2a
Eternelle-186 Add EF configs and repositories
engrkwakwak May 28, 2026
6022d93
Eternelle-186 Add outbox/inbox processing plumbing
engrkwakwak May 28, 2026
d1e6664
Eternelle-186 Add module DI registration and consumer wiring
engrkwakwak May 28, 2026
cd42c80
Eternelle-186 Consume Catalog manifest into local projection
engrkwakwak May 28, 2026
ae14130
Eternelle-186 Add tenant endpoints
engrkwakwak May 28, 2026
97e9e7f
Eternelle-186 Add module configuration files
engrkwakwak May 28, 2026
a0b5001
Eternelle-186 Wire Tenancy module into the API host
engrkwakwak May 28, 2026
cc797e9
Eternelle-186 Add InitialTenancy migration
engrkwakwak May 28, 2026
cab5daf
Eternelle-186 Replace WeddingPlan with Tenancy-derived guest-photo li…
engrkwakwak May 28, 2026
f67949a
Eternelle-186 Seed tenant:edit and tenant:publish permissions
engrkwakwak May 28, 2026
a50818c
Eternelle-186 Apply code review fixes
engrkwakwak May 28, 2026
14c8872
Eternelle-186 Use TenantSlug.MinLength in TooShort error message
engrkwakwak May 28, 2026
0684018
Eternelle-186 Add TenancyDbContext to migration extensions
engrkwakwak May 28, 2026
c905252
Merge pull request #187 from engrkwakwak/eternelle-186
engrkwakwak May 28, 2026
c4c932b
Eternelle-189 Update docs: modules complete, 5-project structure, VOs
engrkwakwak May 28, 2026
e303349
Eternelle-189 Update docs for RSVP module, domain rules, and endpoints
engrkwakwak May 28, 2026
42cac9f
Merge pull request #190 from engrkwakwak/eternelle-189
engrkwakwak May 28, 2026
016c187
Eternelle-191 Add InternalsVisibleTo for Weddings + Rsvp infrastructure
engrkwakwak May 29, 2026
acb010b
Eternelle-191 Lift Person* VOs from Weddings to Common.Domain.People
engrkwakwak May 29, 2026
1e26a95
Eternelle-191 Refactor(common-domain): lift RichDescription to Common…
engrkwakwak May 29, 2026
be2bb93
Eternelle-191 Refactor(common-domain): lift GroupLabel and InternalNo…
engrkwakwak May 29, 2026
d1d4f08
Eternelle-191 Add EmailAddress and PhoneNumber VOs
engrkwakwak May 29, 2026
6c75477
Eternelle-191 Scaffold five-project module skeleton
engrkwakwak May 29, 2026
8438ae1
Eternelle-191 Add IDs and shared VOs (LookupCode, DietaryNote)
engrkwakwak May 29, 2026
dd13ec5
Eternelle-191 feat(rsvp-domain): add Wedding mirror entity + IWedding…
engrkwakwak May 29, 2026
d4591cf
Eternelle-191 Add Guest aggregate, domain events, repository interface
engrkwakwak May 29, 2026
9f2ca52
Eternelle-191 Add RsvpSubmission aggregate + companion child + domain…
engrkwakwak May 29, 2026
707fb8d
Eternelle-191 Add GuestAdded + RsvpSubmitted integration events
engrkwakwak May 29, 2026
1e8ac0b
Eternelle-191 Add AssemblyReference + Application abstractions
engrkwakwak May 29, 2026
aacce4f
Eternelle-191 Add AssemblyReference, Tags, RateLimitingPolicies
engrkwakwak May 29, 2026
1b6e965
Eternelle-191 Add DbContext, ID converters, design-time factory
engrkwakwak May 29, 2026
27f8463
Eternelle-191Add EF entity configurations
engrkwakwak May 29, 2026
b499a6b
Eternelle-191 Add EF configurations, repositories, Wedding projection
engrkwakwak May 29, 2026
2f64566
Eternelle-191 Clone Inbox/Outbox pipeline from Tenancy
engrkwakwak May 29, 2026
8d57012
Eternelle-191 Add RsvpModule DI registration
engrkwakwak May 29, 2026
f7e78c9
Eternelle-191 Add LookupCodeGenerator, complete infrastructure
engrkwakwak May 29, 2026
13f628d
Eternelle-191 Register Rsvp module + rate-limit policies
engrkwakwak May 29, 2026
3c7786c
Eternelle-191 Add modules.rsvp.json config
engrkwakwak May 29, 2026
1d39c36
Eternelle-191 Initial migration + register with MigrationExtensions
engrkwakwak May 29, 2026
5d01e26
Eternelle-191 Consume WeddingCreatedIntegrationEvent into rsvp.weddin…
engrkwakwak May 29, 2026
21a0b58
Eternelle-191 AddGuest command
engrkwakwak May 29, 2026
0b06c1b
Eternelle-191 UpdateGuestContact command
engrkwakwak May 29, 2026
edb7e61
Eternelle-191 ChangeGuestAllowance command
engrkwakwak May 29, 2026
2198945
Eternelle-191 UpdateGuestPlacement command
engrkwakwak May 29, 2026
7c94d81
Eternelle-191 LockGuest + UnlockGuest commands
engrkwakwak May 29, 2026
85c8ec0
Eternelle-191 RegenerateGuestToken command
engrkwakwak May 29, 2026
f9e21bb
Eternelle-191 RemoveGuest command
engrkwakwak May 29, 2026
5e76e1b
Eternelle-191 GetGuest query
engrkwakwak May 29, 2026
47a2832
Eternelle-191 ListGuests query
engrkwakwak May 29, 2026
f850889
Eternelle-191 ListGuests style fixes (IDE0007, IDE0031)
engrkwakwak May 29, 2026
5b87de7
Eternelle-191 SetRsvpDeadline + ClearRsvpDeadline commands
engrkwakwak May 29, 2026
ea70a12
Eternelle-191 GetInvitation public query
engrkwakwak May 29, 2026
dd25f2e
Eternelle-191 SubmitRsvp public command
engrkwakwak May 29, 2026
0b13a44
Eternelle-191 SubmitOnBehalf command
engrkwakwak May 29, 2026
8fce1f9
Eternelle-191 Submission queries (GetSubmission, ListSubmissions, Get…
engrkwakwak May 29, 2026
4711944
Eternelle-191 Guest CRUD endpoints
engrkwakwak May 29, 2026
22a9d82
Eternelle-191 Submission endpoints
engrkwakwak May 29, 2026
09330ac
Eternelle-191 Wedding deadline endpoints
engrkwakwak May 29, 2026
e173eec
Eternelle-191 Public invitation + submit endpoints
engrkwakwak May 29, 2026
ab613a6
Eternelle-191 Update schema: Tier, PriceClass, SectionId to integers
engrkwakwak May 29, 2026
a2526bb
Eternelle-191 Replace database-design.md with system architecture.md
engrkwakwak May 29, 2026
31aaba6
Eternelle-191 Remove weddings inbox/outbox plan; update logging filters
engrkwakwak May 29, 2026
ee60578
Eternelle-191 Refactor RSVP queries to use Dapper and SQL projections
engrkwakwak May 30, 2026
7db7476
Eternelle-191 Add CancellationToken support to DB ops, minor cleanups
engrkwakwak May 30, 2026
a2135df
Eternelle-191 Use CommandDefinition in QueryAsync for better handling
engrkwakwak May 30, 2026
30cc52b
Merge pull request #192 from engrkwakwak/eternelle-191
engrkwakwak May 30, 2026
02e50ba
Eternelle-194 Add guest names to GuestAdded integration event
engrkwakwak May 30, 2026
062b1ae
Eternelle-194 Publish guest lookup-code-regenerated integration event
engrkwakwak May 30, 2026
3fc8284
Eternelle-194 Publish guest-removed integration event
engrkwakwak May 30, 2026
43be503
Eternelle-194 Rename SnapShareConfig to SocialShareConfig and drop up…
engrkwakwak May 30, 2026
4c2d436
Eternelle-194 Rename SnapShareResponse to SocialShareResponse
engrkwakwak May 30, 2026
6548ff8
Eternelle-194 Remove guest-photo upload subsystem from Weddings
engrkwakwak May 30, 2026
30f07d3
Eternelle-194 Migrate Weddings: drop guest photos and rename social s…
engrkwakwak May 31, 2026
8a1b346
Eternelle-194 Scaffold Album module five projects
engrkwakwak May 31, 2026
d9ed2e3
Eternelle-194 Add Album schema constant and shared value objects
engrkwakwak May 31, 2026
28e631f
Eternelle-194 Add Album enums and photo id
engrkwakwak May 31, 2026
7ca1870
Eternelle-194 Add AlbumSettings aggregate
engrkwakwak May 31, 2026
904ebe1
Eternelle-194 Add Album invitee mirror
engrkwakwak May 31, 2026
2ea1b55
Eternelle-194 Add AlbumPhoto aggregate, events, errors and repository…
engrkwakwak May 31, 2026
40264ab
Eternelle-194 Move photo storage and slot store into Album
engrkwakwak May 31, 2026
8c8fecf
Eternelle-194 Add Album DbContext and messaging infrastructure
engrkwakwak May 31, 2026
80846b6
Eternelle-194 Add Album EF configurations and repositories
engrkwakwak May 31, 2026
60e55b7
Eternelle-194 Publish Album photo integration events
engrkwakwak May 31, 2026
c1e08e9
Eternelle-194 Register Album module infrastructure
engrkwakwak May 31, 2026
3b1d615
Eternelle-194 Add shared Album upload service
engrkwakwak May 31, 2026
7c96c04
Eternelle-194 Add Album guest upload commands and context query
engrkwakwak May 31, 2026
9ec65cb
Eternelle-194 Add Album host upload commands
engrkwakwak May 31, 2026
d86ffae
Eternelle-194 Add Album moderation, feed and settings handlers
engrkwakwak May 31, 2026
93c2dac
Eternelle-194 Add Album tags and rate-limit policy names
engrkwakwak May 31, 2026
4e77f43
Eternelle-194 Add Album guest endpoints
engrkwakwak May 31, 2026
56a7b8a
Eternelle-194 Add Album host, moderation, feed and settings endpoints
engrkwakwak May 31, 2026
bf51bfe
Eternelle-194 Consume wedding, tenant and RSVP guest events in Album
engrkwakwak May 31, 2026
ff374da
Eternelle-194 Register Album module in the API host
engrkwakwak May 31, 2026
93e273b
Eternelle-194 Add Album module configuration
engrkwakwak May 31, 2026
e08c9df
Eternelle-194 Add initial Album migration
engrkwakwak May 31, 2026
d9874ac
Eternelle-194 Seed album:manage permission for Couple and Admin
engrkwakwak May 31, 2026
c133807
Eternelle-194 Fix TenantId index uniqueness and multi-wedding upload …
engrkwakwak May 31, 2026
8925dab
Eternelle-194 Suppress auto-generated analyzer warnings in Album migr…
engrkwakwak May 31, 2026
bb6f728
Eternelle-194 Rename UpdateSnapShareConfig to UpdateSocialShareConfig…
engrkwakwak May 31, 2026
cb8a2ee
Eternelle-194 Refactor Album module: Dapper, validation, and cleanup
engrkwakwak May 31, 2026
e7a8395
Eternelle-194 Refactor Invitee config and simplify error logging
engrkwakwak May 31, 2026
285843b
Merge pull request #195 from engrkwakwak/eternelle-194
engrkwakwak May 31, 2026
72083d1
Eternelle-193 Refactor caching, template, and user registration logic
engrkwakwak May 31, 2026
84e3797
Eternelle-193 Add cancellation support and improve validation
engrkwakwak May 31, 2026
42a6923
Eternelle-193 Refactor RSVP lookup code logic, docs, and validation
engrkwakwak May 31, 2026
f74341e
Eternelle-193 Refactor validation, normalization, and event handling
engrkwakwak May 31, 2026
3d16b4a
Eternelle-193 Refine cache exception handling and guest query validation
engrkwakwak May 31, 2026
78eab1e
Eternelle-193 Allow null PhotoIds in bulk photo command validators
engrkwakwak May 31, 2026
8b4a5fa
Merge pull request #196 from engrkwakwak/eternelle-0193
engrkwakwak May 31, 2026
9781ba0
Eternelle-193 Add RSVP and Album modules; update docs and migrations
engrkwakwak Jun 1, 2026
711023e
Eternelle-193 Add Album and RSVP module documentation
engrkwakwak Jun 1, 2026
8c4098f
Merge pull request #197 from engrkwakwak/eternelle-0193
engrkwakwak Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 0 additions & 9 deletions .claude/settings.local.json

This file was deleted.

13 changes: 7 additions & 6 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,18 @@ reviews:
- name: check conventions
enabled: true
instructions: >
Review the changed .cs files against the conventions in CLAUDE.md.
Check specifically: (1) value objects use const int MaxLength not static readonly,
(2) domain event handlers inject only IEventBus and source payload from the
domain event (no repository reload), (3) every route parent ID is passed
through to the command, (4) IDOR guards return opaque NotFound errors.
Review the changed .cs files for conventions not covered by pre-merge checks.
Check: (1) aggregates expose behaviour through methods only — no public setters
outside the aggregate root, (2) domain errors use Error.NotFound / Error.Problem /
Error.Conflict / Error.Failure from Eternelle.Common.Domain (never raw exceptions),
(3) handlers return Result or Result<T> — no throw for business rule violations,
(4) cross-module references use plain Guid, never a value object from another module.
Post a concise list of any violations found; if none, confirm compliance.

pre_merge_checks:
override_requested_reviewers_only: false
docstrings:
mode: off
mode: warning
title:
mode: warning
requirements: "Title should reference the Linear ticket (e.g. Eternelle-123) and be concise."
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ appsettings.local.json
/create-issues-guest-photos-retry.sh
/create-issues-guest-photos.sh
/create-issues.sh
/docs/superpowers
/.claude/settings.local.json
/.files/eternelle-realm.json
25 changes: 11 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# CLAUDE.md — Eternelle

Modular monolith backend in **.NET 10**, Clean Architecture + full DDD.
Database design: `docs/database-design.md`.
System architecture: `docs/architecture.md`.
Module conventions (authoritative): `docs/module-conventions.md`.
Per-module reference docs: `docs/modules/` (Weddings, Users, Catalog, Tenancy).

## Reference Implementations (Milan Jovanovic)

Expand Down Expand Up @@ -36,7 +37,7 @@ src/
└── Eternelle.Modules.Weddings.IntegrationEvents/ ← integration event classes only
```

Future modules follow the same **five-project** structure: **Identity → Tenancy → RSVP → Guestbook Catalog**.
Future modules follow the same **five-project** structure: **UsersCatalog → Tenancy → RSVP → Guestbook**. Users, Catalog, and Tenancy are complete.

---

Expand All @@ -45,11 +46,11 @@ Future modules follow the same **five-project** structure: **Identity → Tenanc
| Module | Domain | Application | Infrastructure | Presentation | IntegrationEvents |
|-----------|--------|-------------|----------------|--------------|-------------------|
| Weddings | ✅ | ✅ | ✅ | ✅ | ✅ |
| Identity | ❌ | ❌ | ❌ | ❌ | ❌ |
| Tenancy | ❌ | ❌ | ❌ | ❌ | ❌ |
| Users | ✅ | ✅ | ✅ | ✅ | ✅ |
| Catalog | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tenancy | ✅ | ✅ | ✅ | ✅ | ✅ |
| RSVP | ❌ | ❌ | ❌ | ❌ | ❌ |
| Guestbook | ❌ | ❌ | ❌ | ❌ | ❌ |
| Catalog | ❌ | ❌ | ❌ | ❌ | ❌ |

---

Expand All @@ -69,10 +70,10 @@ Future modules follow the same **five-project** structure: **Identity → Tenanc
**Aggregate roots** extend `Eternelle.Common.Domain.Entity`.

```csharp
// Strongly-typed ID — always a readonly record struct
// Strongly-typed ID — always a readonly record struct, UUIDv7 via the BCL
public readonly record struct WeddingId(Guid Value)
{
public static WeddingId New() => new(Guid.NewGuid());
public static WeddingId New() => new(Guid.CreateVersion7());
public static WeddingId Empty => new(Guid.Empty);
}

Expand Down Expand Up @@ -269,14 +270,10 @@ internal sealed class CreateWeddingEndpoint : IEndpoint

---

## Next Module: Identity
## Next Module: RSVP

Module name: **`Eternelle.Modules.Identity`** — not IAM.
Schema constant: `public static class Schemas { public const string Identity = "identity"; }`

Rationale: matches `.NET` ecosystem convention (ASP.NET Core Identity), correct scope (auth + user profile), avoids acronym in namespace, already referenced as the example in `docs/module-conventions.md`.

Cross-module data flow: `WeddingCreatedIntegrationEvent` → Identity handler stores a local copy of `WeddingId`/`TenantId`/`WeddingDate` in the `identity` schema. Identity never queries the `wedding` schema directly.
Module name: **`Eternelle.Modules.Rsvp`**
Schema constant: `public static class Schemas { public const string Rsvp = "rsvp"; }`

---

Expand Down
47 changes: 31 additions & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@ This document describes the architecture rules, coding conventions, and patterns

## Layer rules

Each module has four layers. The dependency rule is strict and enforced by project references.
Each module is a **five-project** vertical slice: `Domain`, `Application`, `Infrastructure`, `Presentation`, and `IntegrationEvents`. The dependency rule is strict and enforced by project references — only a module's `IntegrationEvents` project may be referenced from outside the module. See [`docs/module-conventions.md`](docs/module-conventions.md) for the full reference graph.

### Domain

**Zero dependencies on Application, Infrastructure, or any framework** (no EF Core, MediatR, Dapper, ASP.NET).

- Aggregates expose behaviour through methods only — no public setters outside the aggregate root.
- Owned entities (e.g. `EntourageMember`, `Partner`) are created and mutated exclusively through their aggregate root. Their factory and mutation methods are `internal`.
- Value objects are `sealed record` or `sealed class` with a private constructor and a `static Create()` factory returning `Result<T>`.
- String length limits are defined as `public static readonly int` constants on the owning type, not as magic numbers:
- Value objects are `sealed record` with a private constructor, a `static Create()` factory returning `Result<T>`, an `internal static FromPersistence()` (trusts the DB, skips validation), and a sibling `{Vo}Errors` class.
- String length limits are defined as a `public const int MaxLength` on the value object, not as magic numbers:

```csharp
// On EntourageMember:
public static readonly int MaxNameLength = 150;
public static readonly int MaxRoleLength = 100;
// On PersonName:
public const int MaxLength = 150;
```

`const` (not `static readonly`) is correct here: this is a monorepo where every project compiles from source, so there is no cross-assembly binary-inlining concern.
- Domain errors use the `Error` type from `Eternelle.Common.Domain`:

```csharp
public static Error NotFound(WeddingId id) =>
Error.NotFound("Weddings.NotFound", $"Wedding {id.Value} was not found");
```

- Domain events are raised via `Entity.Raise()` and are plain records implementing `IDomainEvent`.

### Application
Expand All @@ -35,21 +39,26 @@ Each module has four layers. The dependency rule is strict and enforced by proje
- Commands implement `ICommand` (void) or `ICommand<TResponse>`.
- Queries implement `IQuery<TResponse>`.
- All handlers are `internal sealed class`.
- Every command must have a matching `AbstractValidator<TCommand>`. `MaximumLength` rules **must** reference domain constants:
- Every command must have a matching `AbstractValidator<TCommand>`. `MaximumLength` rules **must** reference the value object's `MaxLength` constant (the single source of truth):

```csharp
RuleFor(c => c.Name)
.NotEmpty()
.MaximumLength(EntourageMember.MaxNameLength);
.MaximumLength(PersonName.MaxLength);
```

- Nullable fields are validated conditionally:

```csharp
RuleFor(c => c.Subtitle)
.MaximumLength(EntourageGroup.MaxSubtitleLength)
.MaximumLength(GroupSubtitle.MaxLength)
.When(c => c.Subtitle is not null);
```

- **Query handlers use Dapper** via `IDbConnectionFactory` with `CommandDefinition` (always pass `cancellationToken`). Never use EF Core in a query handler.
- **Command handlers use domain repository interfaces** (`IWeddingRepository`, etc.) and `IUnitOfWork`. Call `entourageGroupRepository.Update(group)` after mutating owned entities, then `await unitOfWork.SaveChangesAsync(cancellationToken)`.
- Reorder handlers **must** validate list completeness before iterating using a three-condition HashSet check:

```csharp
HashSet<Guid> providedIds = [.. command.StoryMomentIds];
if (command.StoryMomentIds.Count != providedIds.Count || // duplicate check
Expand All @@ -65,15 +74,20 @@ Each module has four layers. The dependency rule is strict and enforced by proje
- EF Core entity configurations go in `IEntityTypeConfiguration<T>` classes, one per aggregate root.
- `UseSnakeCaseNamingConvention()` is active. Use explicit `HasColumnName()` only when the snake_case default is wrong.
- All strongly-typed ID value objects must have a `ValueConverter` registered globally in `ConfigureConventions`, not per-property:

```csharp
builder.Properties<WeddingId>().HaveConversion<WeddingIdConverter>();
```

- String length constants from the domain must be referenced in `HasMaxLength()` — no magic numbers:

```csharp
.HasMaxLength(EntourageMember.MaxNameLength)
.HasMaxLength(PersonName.MaxLength)
```

- Enums are persisted as integers via `HasConversion<int>()` (with a `CHECK` or unique constraint where a closed set must be enforced at the DB).
- Repositories are `internal sealed class` implementing domain interfaces.
- All tables live in the `wedding` schema. Declare it via `modelBuilder.HasDefaultSchema(Schemas.Weddings)`.
- Each module declares its own schema via `modelBuilder.HasDefaultSchema(Schemas.{Module})` (`wedding`, `users`, `catalog`).

### Presentation

Expand Down Expand Up @@ -105,16 +119,17 @@ if (result.IsFailure) return result;

## ID types

Every aggregate and owned entity uses a strongly-typed ID record:
Every aggregate and owned entity uses a strongly-typed ID `readonly record struct`:

```csharp
public record WeddingId(Guid Value)
public readonly record struct WeddingId(Guid Value)
{
public static WeddingId New() => new(Uuid7.NewUuid7().ToGuid());
public static WeddingId New() => new(Guid.CreateVersion7());
public override string ToString() => Value.ToString();
}
```

IDs use **UUIDv7** (time-ordered) via `SourceFlow.Stores.EntityFramework`. This keeps database index locality while remaining globally unique.
IDs use **UUIDv7** (time-ordered) via the BCL `Guid.CreateVersion7()` (.NET 9+). This keeps database index locality while remaining globally unique. A `ValueConverter` for each ID type is registered globally in `ConfigureConventions`.

---

Expand Down Expand Up @@ -209,7 +224,7 @@ migrationBuilder.Sql("""
- All handler classes are `internal sealed`.
- Command and query records are `public sealed record`.
- Endpoint classes are `internal sealed` implementing `IEndpoint`.
- String constants on domain types use `public static readonly int`, not `const int`, because `const` fields are inlined at compile time and can cause issues across assembly boundaries.
- String length limits on value objects use `public const int MaxLength`. `const` is correct in this monorepo (all projects compile from source); `static readonly` would only be needed if domain types shipped as binary NuGet packages to independently-deployed consumers.
- Nullable reference types are enabled project-wide. Never suppress the nullable warning without a comment explaining why.
- `using` directives are auto-managed by the IDE (file-scoped namespaces, no redundant usings).
- Diagnostic suppressions (`NoWarn`, `#pragma warning disable`) go in `.editorconfig`, not in `.csproj` files or inline in source.
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PackageVersion Include="MassTransit" Version="9.1.0" />
<PackageVersion Include="MassTransit.Redis" Version="9.1.0" />
<PackageVersion Include="MediatR" Version="14.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Http" Version="2.3.9" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.4" />
Expand Down
67 changes: 66 additions & 1 deletion Eternelle.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
<Solution>
<Folder Name="/docs/">
<File Path="docs/architecture.md" />
<File Path="docs/feature-gift-contributions.md" />
<File Path="docs/feature-guest-photo-upload.md" />
<File Path="docs/module-conventions.md" />
</Folder>
<Folder Name="/docs/modules/">
<File Path="docs/modules/album.md" />
<File Path="docs/modules/catalog.md" />
<File Path="docs/modules/README.md" />
<File Path="docs/modules/rsvp.md" />
<File Path="docs/modules/users.md" />
<File Path="docs/modules/weddings.md" />
</Folder>
<Folder Name="/docs/superpowers/" />
<Folder Name="/docs/superpowers/plans/">
<File Path="docs/superpowers/plans/2026-05-20-users-module.md" />
<File Path="docs/superpowers/plans/2026-05-27-catalog-module.md" />
<File Path="docs/superpowers/plans/2026-05-28-tenancy-module.md" />
<File Path="docs/superpowers/plans/2026-05-29-rsvp-module.md" />
<File Path="docs/superpowers/plans/2026-05-30-album-module.md" />
</Folder>
<Folder Name="/docs/superpowers/specs/">
<File Path="docs/superpowers/specs/2026-05-20-users-module-design.md" />
<File Path="docs/superpowers/specs/2026-05-27-catalog-module-design.md" />
<File Path="docs/superpowers/specs/2026-05-28-tenancy-module-design.md" />
<File Path="docs/superpowers/specs/2026-05-29-rsvp-module-design.md" />
<File Path="docs/superpowers/specs/2026-05-30-album-module-design.md" />
<File Path="docs/superpowers/specs/2026-05-30-guestbook-module-design.md" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path="Directory.Build.props" />
Expand All @@ -15,7 +45,42 @@
<Project Path="src/Common/Eternelle.Common.Presentation/Eternelle.Common.Presentation.csproj" Id="54cfc49d-e0bb-44a0-84ac-ee35515c1b5e" />
</Folder>
<Folder Name="/src/Modules/" />
<Folder Name="/src/Modules/Wedding/">
<Folder Name="/src/Modules/Album/">
<Project Path="src/Modules/Album/Eternelle.Modules.Album.Application/Eternelle.Modules.Album.Application.csproj" />
<Project Path="src/Modules/Album/Eternelle.Modules.Album.Domain/Eternelle.Modules.Album.Domain.csproj" />
<Project Path="src/Modules/Album/Eternelle.Modules.Album.Infrastructure/Eternelle.Modules.Album.Infrastructure.csproj" />
<Project Path="src/Modules/Album/Eternelle.Modules.Album.IntegrationEvents/Eternelle.Modules.Album.IntegrationEvents.csproj" />
<Project Path="src/Modules/Album/Eternelle.Modules.Album.Presentation/Eternelle.Modules.Album.Presentation.csproj" />
</Folder>
<Folder Name="/src/Modules/Catalog/">
<Project Path="src/Modules/Catalog/Eternelle.Modules.Catalog.Application/Eternelle.Modules.Catalog.Application.csproj" />
<Project Path="src/Modules/Catalog/Eternelle.Modules.Catalog.Domain/Eternelle.Modules.Catalog.Domain.csproj" />
<Project Path="src/Modules/Catalog/Eternelle.Modules.Catalog.Infrastructure/Eternelle.Modules.Catalog.Infrastructure.csproj" />
<Project Path="src/Modules/Catalog/Eternelle.Modules.Catalog.IntegrationEvents/Eternelle.Modules.Catalog.IntegrationEvents.csproj" />
<Project Path="src/Modules/Catalog/Eternelle.Modules.Catalog.Presentation/Eternelle.Modules.Catalog.Presentation.csproj" />
</Folder>
<Folder Name="/src/Modules/Rsvp/">
<Project Path="src/Modules/Rsvp/Eternelle.Modules.Rsvp.Application/Eternelle.Modules.Rsvp.Application.csproj" Id="584d8710-9449-4431-9359-59ec784eb276" />
<Project Path="src/Modules/Rsvp/Eternelle.Modules.Rsvp.Domain/Eternelle.Modules.Rsvp.Domain.csproj" Id="73e928a0-583f-4b26-93de-d541914305d4" />
<Project Path="src/Modules/Rsvp/Eternelle.Modules.Rsvp.Infrastructure/Eternelle.Modules.Rsvp.Infrastructure.csproj" Id="2c5fbf2f-a4ee-46ed-beb7-d76408d75222" />
<Project Path="src/Modules/Rsvp/Eternelle.Modules.Rsvp.IntegrationEvents/Eternelle.Modules.Rsvp.IntegrationEvents.csproj" Id="55dc7f38-3a59-4077-8249-46530f103444" />
<Project Path="src/Modules/Rsvp/Eternelle.Modules.Rsvp.Presentation/Eternelle.Modules.Rsvp.Presentation.csproj" Id="4f8b50a7-2c4a-46eb-b83a-63007eab9e69" />
</Folder>
<Folder Name="/src/Modules/Tenancy/">
<Project Path="src/Modules/Tenancy/Eternelle.Modules.Tenancy.Application/Eternelle.Modules.Tenancy.Application.csproj" />
<Project Path="src/Modules/Tenancy/Eternelle.Modules.Tenancy.Domain/Eternelle.Modules.Tenancy.Domain.csproj" />
<Project Path="src/Modules/Tenancy/Eternelle.Modules.Tenancy.Infrastructure/Eternelle.Modules.Tenancy.Infrastructure.csproj" />
<Project Path="src/Modules/Tenancy/Eternelle.Modules.Tenancy.IntegrationEvents/Eternelle.Modules.Tenancy.IntegrationEvents.csproj" />
<Project Path="src/Modules/Tenancy/Eternelle.Modules.Tenancy.Presentation/Eternelle.Modules.Tenancy.Presentation.csproj" />
</Folder>
<Folder Name="/src/Modules/Users/">
<Project Path="src/Modules/Users/Eternelle.Modules.Users.Application/Eternelle.Modules.Users.Application.csproj" Id="27530601-1743-4d73-80d3-bddbf28d8eb2" />
<Project Path="src/Modules/Users/Eternelle.Modules.Users.Domain/Eternelle.Modules.Users.Domain.csproj" Id="823bb7bc-2382-4588-a404-73ae27f49b02" />
<Project Path="src/Modules/Users/Eternelle.Modules.Users.Infrastructure/Eternelle.Modules.Users.Infrastructure.csproj" Id="290675fd-c75d-44e0-8cdc-52999a9ead97" />
<Project Path="src/Modules/Users/Eternelle.Modules.Users.IntegrationEvents/Eternelle.Modules.Users.IntegrationEvents.csproj" Id="f4f38cd7-8af7-44a6-a6aa-4b57bb9dc00d" />
<Project Path="src/Modules/Users/Eternelle.Modules.Users.Presentation/Eternelle.Modules.Users.Presentation.csproj" Id="d6dde47a-faab-4b24-9738-6fe5d9e71738" />
</Folder>
<Folder Name="/src/Modules/Weddings/">
<Project Path="src/Modules/Weddings/Eternelle.Modules.Weddings.Application/Eternelle.Modules.Weddings.Application.csproj" Id="a6f4ff32-35b6-40bf-a034-4aff406678ea" />
<Project Path="src/Modules/Weddings/Eternelle.Modules.Weddings.Domain/Eternelle.Modules.Weddings.Domain.csproj" Id="8ca34ec3-032b-4439-bc9f-318df0f9e87a" />
<Project Path="src/Modules/Weddings/Eternelle.Modules.Weddings.Infrastructure/Eternelle.Modules.Weddings.Infrastructure.csproj" Id="f7f463ef-3373-4063-9909-487fb52a34e0" />
Expand Down
Loading