feat(identity): Avro events + per-module tenant provisioning (Phase 2)#93
Conversation
Completes the PR #92 follow-up by replacing the central TenantSchemaProvisioner with an event-driven per-module flow: - Identity raises 5 domain events (OrganizationVerified, UserDeactivated, UserReactivated, RoleAssigned, RoleRemoved). IdentityUnitOfWork maps them to Avro integration events and publishes via Wolverine outbox → Kafka with CloudEvents envelope (ADR-019). - Each of DataModeling/FormBuilder/WorkflowBuilder/WorkflowEngine owns an OrganizationVerifiedHandler that provisions its own tenant schema (CREATE SCHEMA IF NOT EXISTS + MigrateAsync) when the event arrives. - Removed: central TenantSchemaProvisioner, ProvisionTenantHandler, ProvisionTenantMessage, ITenantProvisioningScheduler, WolverineTenantProvisioningScheduler. Extraction of a module is now a redeploy of that module's own handler + provisioner — no refactor of the gateway (ADR-010). - Axis.Api wires AddGrpcClient<IdentityServiceClient> against Modules:Identity:GrpcUrl (defaults to self-loop for modulith). Same call site survives extraction. - Added patterns.md § Pattern 3 (JWKS-only JWT validation) as the rule for consuming modules: never call IdentityDbContext per request; validate locally via Identity's JWKS endpoint. Closes the deferred line in PROGRESS.md and E02 README. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…04/E05/E06 PR #93 moved tenant schema provisioning from a central provisioner in Axis.Api to per-module OrganizationVerifiedHandler instances in each module's Infrastructure project. Update the implementation-status tables in the touched epic READMEs so docs reflect the new ownership. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthroughThis PR replaces centralized tenant provisioning with Identity domain events, Avro/Kafka publication, and per-module ChangesEvent-driven tenant provisioning
Sequence Diagram(s)sequenceDiagram
participant User as User
participant IdentityUnitOfWork as IdentityUnitOfWork
participant IdentityEventMapper as IdentityEventMapper
participant Wolverine as Wolverine
participant Kafka as Kafka
participant OrganizationVerifiedHandler as OrganizationVerifiedHandler
participant PostgreSQL as PostgreSQL
User->>IdentityUnitOfWork: VerifyEmail / other domain changes
IdentityUnitOfWork->>IdentityEventMapper: ToIntegrationEvent(domainEvent)
IdentityEventMapper-->>IdentityUnitOfWork: Avro event payload
IdentityUnitOfWork->>Wolverine: PublishAsync(mapped event)
Wolverine->>Kafka: publish topic message
Kafka->>OrganizationVerifiedHandler: OrganizationVerifiedEvent
OrganizationVerifiedHandler->>PostgreSQL: CREATE SCHEMA IF NOT EXISTS
OrganizationVerifiedHandler->>PostgreSQL: Database.MigrateAsync()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Markdown link check on PR #93 failed because `docs/playbooks/local-dev.md` linked to `src/Axis.Api/Infrastructure/TenantSchemaProvisioner.cs`, which was deleted when ownership moved into each module. Replace the dead link and rewrite the surrounding paragraphs in `local-dev.md`, `patterns.md` § Tenant schema provisioning, and `docs/epics/E01-platform-foundation/features/F01-tenant-registration.md` to describe the new per-module OrganizationVerifiedHandler flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
src/Axis.Api/Program.cs (1)
320-325: ⚡ Quick winReplace the fully-qualified gRPC client type with a
usingalias.Line 322 hardcodes the nested type name in the registration. Please add a top-level alias/import and register that alias instead so this stays consistent with the repo's C# import style.
♻️ Proposed cleanup
+using IdentityServiceClient = Axis.Identity.Contracts.Grpc.IdentityService.IdentityServiceClient; ... -builder.Services.AddGrpcClient<Axis.Identity.Contracts.Grpc.IdentityService.IdentityServiceClient>(opts => +builder.Services.AddGrpcClient<IdentityServiceClient>(opts => { opts.Address = new Uri(identityGrpcUrl); });As per coding guidelines, "Use using declarations instead of fully-qualified names for imports in C# files."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Axis.Api/Program.cs` around lines 320 - 325, The registration currently uses the fully-qualified nested type Axis.Identity.Contracts.Grpc.IdentityService.IdentityServiceClient; add a top-level using alias (e.g., a using declaration alias for IdentityServiceClient) and replace the fully-qualified type in AddGrpcClient with that alias (referencing the identityGrpcUrl variable and the AddGrpcClient registration) so the file follows the repo's import style and avoids the long nested type name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/PROGRESS.md`:
- Line 74: Replace the nonconforming deferred callout "**Deferred:**" in the
PROGRESS.md entry that starts with "**Verify email → provision (PR `#93`):**" with
the required format; update it to "**Deferred (PR `#93` follow-up):**" (or
"**Deferred (follow-up PR):**" if no PR number is intended) so it matches the
coding guideline, ensuring the rest of the sentence ("retry/backoff/alert on
provision failures, provisioning wait UI, Admin role on verify per E01 US-003")
remains unchanged; look for the literal "**Deferred:**" in the Verify email →
provision paragraph and perform this single-token replacement.
In
`@src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`:
- Around line 28-38: The migration block can run concurrently for the same
organization causing migration/DDL races; wrap schema creation and await
context.Database.MigrateAsync(...) behind a per-organization lock (e.g., acquire
a PostgreSQL advisory lock on a hash of organizationId using the existing
NpgsqlConnection before CREATE SCHEMA and MigrateAsync, or use a distributed
lock/Redis keyed by organizationId), hold the lock for the duration of schema
creation + new DataModelingDbContext/Database.MigrateAsync, implement a
timeout/retry and ensure the lock is always released in a finally block;
reference the existing symbols NpgsqlConnection connection, organizationId,
schema, FixedTenantContext and DataModelingDbContext when adding the lock.
In
`@src/Modules/FormBuilder/Axis.FormBuilder.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`:
- Around line 27-37: The migration path in OrganizationVerifiedHandler currently
only guards CREATE SCHEMA but allows concurrent calls to
FormBuilderDbContext.Database.MigrateAsync leading to migration/DDL races; wrap
the schema creation and the call to Database.MigrateAsync in a per-organization
lock/dedupe (e.g. a Postgres advisory lock using the organizationId, or an
application-level concurrent dictionary/semaphore keyed by organizationId) so
only one handler runs migration for a given tenant at a time; apply the lock
around the block that opens the NpgsqlConnection, executes
createSchema.CommandText and invokes new
FormBuilderDbContext(...).Database.MigrateAsync to ensure exclusive provisioning
for that FixedTenantContext/organization.
In
`@src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityUnitOfWork.cs`:
- Line 57: The PublishAsync call in IdentityUnitOfWork.cs does not forward the
CancellationToken, so message publishing continues after SaveChangesAsync is
cancelled; update the call site that currently invokes
bus.PublishAsync(integrationEvent) to pass the CancellationToken parameter from
the surrounding async method (e.g., use the method's cancellationToken argument)
so it becomes bus.PublishAsync(integrationEvent, cancellationToken), ensuring
the token is propagated through PublishAsync.
In
`@src/Modules/WorkflowBuilder/Axis.WorkflowBuilder.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`:
- Around line 27-37: The schema provisioning and migration in
OrganizationVerifiedHandler is not serialized and can race when the same
OrganizationVerifiedEvent is redelivered; wrap the CREATE SCHEMA +
Database.MigrateAsync sequence with a per-organization lock/dedupe (for example
acquire a Postgres advisory lock derived from organizationId or use a
distributed mutex keyed by organizationId) before opening/using the
NpgsqlConnection and releasing it after migrations complete; ensure the lock is
held across the createSchema/WorkflowBuilderDbContext.Database.MigrateAsync
calls (symbols: NpgsqlConnection, createSchema.CommandText, FixedTenantContext,
WorkflowBuilderDbContext, Database.MigrateAsync) and make the lock acquisition
retry-safe and cancellable with the existing cancellationToken.
In
`@src/Modules/WorkflowEngine/Axis.WorkflowEngine.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`:
- Around line 27-37: The CREATE SCHEMA + Database.MigrateAsync call in
OrganizationVerifiedHandler can run concurrently for the same organization; wrap
schema provisioning and context.Database.MigrateAsync in a per-organization lock
(or dedupe) to serialize provisioning. Specifically, before running the CREATE
SCHEMA / new FixedTenantContext + WorkflowEngineDbContext and calling
context.Database.MigrateAsync, acquire a lock keyed by organizationId (for
example a Postgres advisory lock using organizationId, or an
in-memory/distributed mutex keyed by organizationId), only proceed if the lock
is held, and ensure the lock is released after migration (with proper
timeout/error handling and retry/backoff on contention). Keep the CREATE SCHEMA,
FixedTenantContext, optionsBuilder.UseNpgsql, and context.Database.MigrateAsync
logic inside the locked section so two handlers for the same organization cannot
run migrations in parallel.
---
Nitpick comments:
In `@src/Axis.Api/Program.cs`:
- Around line 320-325: The registration currently uses the fully-qualified
nested type Axis.Identity.Contracts.Grpc.IdentityService.IdentityServiceClient;
add a top-level using alias (e.g., a using declaration alias for
IdentityServiceClient) and replace the fully-qualified type in AddGrpcClient
with that alias (referencing the identityGrpcUrl variable and the AddGrpcClient
registration) so the file follows the repo's import style and avoids the long
nested type name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ad1aea43-8532-4412-8b3d-44ef5a57ec82
⛔ Files ignored due to path filters (5)
src/Modules/Identity/Axis.Identity.Contracts/Generated/axis/identity/events/OrganizationVerifiedEvent.csis excluded by!**/generated/**src/Modules/Identity/Axis.Identity.Contracts/Generated/axis/identity/events/RoleAssignedEvent.csis excluded by!**/generated/**src/Modules/Identity/Axis.Identity.Contracts/Generated/axis/identity/events/RoleRemovedEvent.csis excluded by!**/generated/**src/Modules/Identity/Axis.Identity.Contracts/Generated/axis/identity/events/UserDeactivatedEvent.csis excluded by!**/generated/**src/Modules/Identity/Axis.Identity.Contracts/Generated/axis/identity/events/UserReactivatedEvent.csis excluded by!**/generated/**
📒 Files selected for processing (47)
Directory.Packages.propsdocs/PROGRESS.mddocs/epics/E01-platform-foundation/README.mddocs/epics/E02-identity-access/README.mddocs/epics/E03-data-modeling/README.mddocs/epics/E04-workflow-builder/README.mddocs/epics/E05-form-builder/README.mddocs/epics/E06-workflow-engine/README.mddocs/playbooks/patterns.mdsrc/Axis.Api/Axis.Api.csprojsrc/Axis.Api/Infrastructure/Handlers/ProvisionTenantHandler.cssrc/Axis.Api/Infrastructure/TenantSchemaProvisioner.cssrc/Axis.Api/Program.cssrc/Modules/DataModeling/Axis.DataModeling.Infrastructure/Axis.DataModeling.Infrastructure.csprojsrc/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cssrc/Modules/FormBuilder/Axis.FormBuilder.Infrastructure/Axis.FormBuilder.Infrastructure.csprojsrc/Modules/FormBuilder/Axis.FormBuilder.Infrastructure/Messaging/OrganizationVerifiedHandler.cssrc/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailHandler.cssrc/Modules/Identity/Axis.Identity.Application/Services/ITenantProvisioningScheduler.cssrc/Modules/Identity/Axis.Identity.Contracts/Axis.Identity.Contracts.csprojsrc/Modules/Identity/Axis.Identity.Contracts/IdentityEventExtensions.cssrc/Modules/Identity/Axis.Identity.Contracts/IdentityKafkaTopics.cssrc/Modules/Identity/Axis.Identity.Contracts/Schemas/OrganizationVerifiedEvent.avscsrc/Modules/Identity/Axis.Identity.Contracts/Schemas/RoleAssignedEvent.avscsrc/Modules/Identity/Axis.Identity.Contracts/Schemas/RoleRemovedEvent.avscsrc/Modules/Identity/Axis.Identity.Contracts/Schemas/UserDeactivatedEvent.avscsrc/Modules/Identity/Axis.Identity.Contracts/Schemas/UserReactivatedEvent.avscsrc/Modules/Identity/Axis.Identity.Domain/Aggregates/User.cssrc/Modules/Identity/Axis.Identity.Domain/Events/OrganizationVerified.cssrc/Modules/Identity/Axis.Identity.Domain/Events/RoleAssigned.cssrc/Modules/Identity/Axis.Identity.Domain/Events/RoleRemoved.cssrc/Modules/Identity/Axis.Identity.Domain/Events/UserDeactivated.cssrc/Modules/Identity/Axis.Identity.Domain/Events/UserReactivated.cssrc/Modules/Identity/Axis.Identity.Infrastructure/Extensions/IdentityInfrastructureExtensions.cssrc/Modules/Identity/Axis.Identity.Infrastructure/Messaging/IdentityEventMapper.cssrc/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityUnitOfWork.cssrc/Modules/Identity/Axis.Identity.Infrastructure/Services/WolverineTenantProvisioningScheduler.cssrc/Modules/WorkflowBuilder/Axis.WorkflowBuilder.Infrastructure/Axis.WorkflowBuilder.Infrastructure.csprojsrc/Modules/WorkflowBuilder/Axis.WorkflowBuilder.Infrastructure/Messaging/OrganizationVerifiedHandler.cssrc/Modules/WorkflowEngine/Axis.WorkflowEngine.Infrastructure/Axis.WorkflowEngine.Infrastructure.csprojsrc/Modules/WorkflowEngine/Axis.WorkflowEngine.Infrastructure/Messaging/OrganizationVerifiedHandler.cssrc/Shared/Axis.Shared.Application/Tenancy/ITenantSchemaProvisioner.cssrc/Shared/Axis.Shared.Application/Tenancy/ProvisionTenantMessage.cssrc/Shared/Axis.Shared.Infrastructure/Tenancy/FixedTenantContext.cstests/Api/Axis.Api.Tests/Helpers/ApiTestFixture.cstests/Api/Axis.Api.Tests/Helpers/NoOpTenantSchemaProvisioner.cstests/Modules/Identity/Axis.Identity.Application.Tests/Commands/VerifyEmailHandlerTests.cs
💤 Files with no reviewable changes (9)
- src/Modules/Identity/Axis.Identity.Infrastructure/Extensions/IdentityInfrastructureExtensions.cs
- src/Shared/Axis.Shared.Application/Tenancy/ProvisionTenantMessage.cs
- src/Modules/Identity/Axis.Identity.Application/Services/ITenantProvisioningScheduler.cs
- src/Shared/Axis.Shared.Application/Tenancy/ITenantSchemaProvisioner.cs
- src/Axis.Api/Infrastructure/Handlers/ProvisionTenantHandler.cs
- tests/Api/Axis.Api.Tests/Helpers/NoOpTenantSchemaProvisioner.cs
- src/Axis.Api/Infrastructure/TenantSchemaProvisioner.cs
- tests/Api/Axis.Api.Tests/Helpers/ApiTestFixture.cs
- src/Modules/Identity/Axis.Identity.Infrastructure/Services/WolverineTenantProvisioningScheduler.cs
| { | ||
| object? integrationEvent = IdentityEventMapper.ToIntegrationEvent(evt); | ||
| if (integrationEvent is not null) | ||
| await bus.PublishAsync(integrationEvent); |
There was a problem hiding this comment.
Forward CancellationToken to PublishAsync.
The PublishAsync call does not forward the CancellationToken parameter. When SaveChangesAsync is cancelled, the message publishing will continue executing instead of being cancelled. As per coding guidelines, always forward CancellationToken through async method calls.
🔧 Proposed fix
- await bus.PublishAsync(integrationEvent);
+ await bus.PublishAsync(integrationEvent, ct);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await bus.PublishAsync(integrationEvent); | |
| await bus.PublishAsync(integrationEvent, ct); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityUnitOfWork.cs`
at line 57, The PublishAsync call in IdentityUnitOfWork.cs does not forward the
CancellationToken, so message publishing continues after SaveChangesAsync is
cancelled; update the call site that currently invokes
bus.PublishAsync(integrationEvent) to pass the CancellationToken parameter from
the surrounding async method (e.g., use the method's cancellationToken argument)
so it becomes bus.PublishAsync(integrationEvent, cancellationToken), ensuring
the token is propagated through PublishAsync.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/playbooks/local-dev.md (1)
86-86: ⚡ Quick winConsider breaking this into multiple sentences for readability.
This single sentence contains three distinct concepts: Identity bootstrap, per-tenant provisioning, and the trigger mechanism. Breaking it into 2-3 shorter sentences would improve scannability.
📝 Suggested rewrite
-On the next boot, `IdentityDbContext.Database.MigrateAsync()` runs at startup (dev only — see [`src/Axis.Api/Program.cs`](../../src/Axis.Api/Program.cs)) to recreate Identity + OpenIddict tables. Per-tenant module schemas (`tenant_{org-id}`) are provisioned on demand by each module's `OrganizationVerifiedHandler` (e.g. [`src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`](../../src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cs)), triggered by Identity's `OrganizationVerifiedEvent` (Kafka, ADR-019). +On the next boot, `IdentityDbContext.Database.MigrateAsync()` runs at startup (dev only — see [`src/Axis.Api/Program.cs`](../../src/Axis.Api/Program.cs)) to recreate Identity + OpenIddict tables. + +Per-tenant module schemas (`tenant_{org-id}`) are provisioned on demand by each module's `OrganizationVerifiedHandler` (e.g. [`src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cs`](../../src/Modules/DataModeling/Axis.DataModeling.Infrastructure/Messaging/OrganizationVerifiedHandler.cs)). This handler is triggered by Identity's `OrganizationVerifiedEvent` published via Kafka (ADR-019).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/playbooks/local-dev.md` at line 86, The long sentence describing startup migration, per-tenant schema provisioning, and the event trigger should be split into 2–3 clearer sentences: one stating that IdentityDbContext.Database.MigrateAsync() runs at startup in dev (reference Program.cs), a second describing that per-tenant module schemas (tenant_{org-id}) are provisioned on demand by each module's OrganizationVerifiedHandler (e.g. OrganizationVerifiedHandler class), and a third noting that this is triggered by Identity's OrganizationVerifiedEvent (Kafka, ADR-019); preserve the existing links and examples while improving scannability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@docs/playbooks/local-dev.md`:
- Line 86: The long sentence describing startup migration, per-tenant schema
provisioning, and the event trigger should be split into 2–3 clearer sentences:
one stating that IdentityDbContext.Database.MigrateAsync() runs at startup in
dev (reference Program.cs), a second describing that per-tenant module schemas
(tenant_{org-id}) are provisioned on demand by each module's
OrganizationVerifiedHandler (e.g. OrganizationVerifiedHandler class), and a
third noting that this is triggered by Identity's OrganizationVerifiedEvent
(Kafka, ADR-019); preserve the existing links and examples while improving
scannability.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8c0498d5-102f-4032-a2b5-8208588fccca
📒 Files selected for processing (3)
docs/epics/E01-platform-foundation/features/F01-tenant-registration.mddocs/playbooks/local-dev.mddocs/playbooks/patterns.md
✅ Files skipped from review due to trivial changes (1)
- docs/epics/E01-platform-foundation/features/F01-tenant-registration.md
Findings actioned:
1. **(Major × 4) Race on __EFMigrationsHistory under Kafka redelivery.**
Each module's OrganizationVerifiedHandler now acquires a session-scoped
Postgres advisory lock keyed by 'axis.tenant.{module}:{org-id}' before
CREATE SCHEMA + MigrateAsync, serializing concurrent provisioning for
the same org. The lock auto-releases when the guard connection closes.
2. **(Minor) PROGRESS.md deferred-callout format.** Replaced bare
'**Deferred:**' with '**Deferred (F01 US-003 follow-up):**' per the
CLAUDE.md Definition of Done convention.
3. **(Nitpick) Long sentence in docs/playbooks/local-dev.md.**
Split into two sentences for scannability — startup migration first,
then per-tenant provisioning flow.
4. **(Major, NOT actioned — false positive)**
'Forward CancellationToken to PublishAsync' in IdentityUnitOfWork.cs.
Wolverine 5.39.3's IMessageBus.PublishAsync signature is
(object, DeliveryOptions?) — no CancellationToken overload exists.
PublishAsync is fire-and-forget onto the outbox. Added an inline
comment mirroring the one already in DataModelingUnitOfWork so this
doesn't get re-flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@coderabbitai Thanks for the review. Addressed in d0d207f:
|
|
Tip For best results, initiate chat on the files or code changes.
2–3. Formatting & prose fixes — noted, all good.
🐇 ✨ ✏️ Learnings added
|
Per PR review discussion: in-file Resolved log is mostly noise. - Git history (`git log -p docs/WORKAROUNDS.md`) is the audit trail. - Durable lessons belong in patterns.md / ADRs where future readers actually look, not in a graveyard at the bottom of the inventory. Updates the "Resolving a workaround" protocol to say: delete the entry when the trigger fires, and only promote the lesson to patterns.md or an ADR if it is durable. Removes the seeded central-tenant-schema- provisioner entry — the PR #93 commit message + patterns.md § Tenant schema provisioning already cover it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CodeRabbit caught the unfilled placeholder. Per CLAUDE.md the literal "PR `#N`" is the template form; when actually using the callout, the substitution should be a real PR number or the responsible feature/US (matches the pattern from PR #93 docs, e.g. "Deferred (F01 US-003 follow-up)"). The deferred items (real IScriptExecutor + INotificationSender) belong to E06 F02-step-handlers — that's where the Script and Notification step semantics are specified. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Completes the PR #92 follow-up. Replaces the central
TenantSchemaProvisionerwith an event-driven per-module flow: Identity raises 5 domain events (OrganizationVerified,UserDeactivated,UserReactivated,RoleAssigned,RoleRemoved);IdentityUnitOfWorkmaps them to Avro integration events and publishes via Wolverine outbox → Kafka with CloudEvents envelope (ADR-019). Each of DataModeling/FormBuilder/WorkflowBuilder/WorkflowEngine now owns anOrganizationVerifiedHandlerin its own Infrastructure project that provisions its own tenant schema — extraction of a module is a redeploy, no gateway refactor (ADR-010).Also wires
AddGrpcClient<IdentityServiceClient>againstModules:Identity:GrpcUrl(self-loop in modulith), and adds JWKS-only validation as a documented P0 rule for consuming modules inpatterns.md(never callIdentityDbContextper request).Linked spec
docs/PROGRESS.md(Phase 2 — Per-module HTTP/gRPC boundary)docs/epics/E02-identity-access/README.md(cleared PR feat(identity): Avro events + per-module tenant provisioning (Phase 2) #93 deferred line)docs/epics/E01-platform-foundation/README.md(tenant provisioning ownership)Requirements & rules followed
Deferred (PR #93 follow-up): Avro user/role lifecycle events, OrganizationVerified Kafka flow, gateway REST → gRPC client wiring, JWKS-only validation doc hardeningfromPROGRESS.mdandE02 README. No new ACs added; no new deferred lines introduced (existing F01 US-003 retry/UI deferred lines preserved).dotnet build Axis.sln(0 errors, 0 warnings);dotnet test Axis.sln(Api 94 / Identity Infra 40 / Identity App 85 / Identity Domain 68 / all other module Infra + unit tests passing);dotnet format --verify-no-changesexit 0.PROGRESS.md,E01-E06READMEs,patterns.mdPattern 3 (JWKS-only)../scripts/check-doc-drift.shgreen.patterns.md§ Rules (P0): "JWT validation is JWKS-only. Consuming modules verify JWTs against Identity's JWKS endpoint locally; never call IdentityDbContext or any Identity service per request just to authenticate a user."TODO/FIXME/NotImplementedException/ placeholder / stub undersrc/,tests/,frontend/src/Summary by CodeRabbit
New Features
Bug Fixes / Changes
Documentation