Skip to content

feat: self-service sign-up to tenant creation end-to-end flow#2126

Merged
bjcoombs merged 4 commits intodevelopfrom
demo-provisioner-fix--9--self-service-signup
Apr 4, 2026
Merged

feat: self-service sign-up to tenant creation end-to-end flow#2126
bjcoombs merged 4 commits intodevelopfrom
demo-provisioner-fix--9--self-service-signup

Conversation

@bjcoombs
Copy link
Copy Markdown
Collaborator

@bjcoombs bjcoombs commented Apr 4, 2026

Summary

  • Defer admin identity creation to a post-provisioning hook when tenant requires async schema provisioning, fixing the broken registration flow where identity writes fail because the tenant schema doesn't exist yet
  • Store self-registered admin credentials (email + bcrypt hash) in tenant metadata during registration, consumed and cleared by the hook after provisioning
  • Add provisioning_pending field to registration response so the frontend knows the tenant is being set up

Changes

Registration handler (services/api-gateway/registration_handler.go):

  • Extend TenantCreator interface to accept metadata and return CreateTenantResult with provisioning status
  • When ProvisioningPending=true: store credentials in metadata, skip inline identity creation
  • When ProvisioningPending=false: create identity inline (existing behavior preserved)
  • Hash password once upfront, reuse for both metadata storage and inline identity creation

Self-registered admin hook (services/identity/bootstrap/self_registered_admin.go):

  • New post-provisioning hook that reads admin email + password hash from tenant metadata
  • Creates identity with tenant-owner role in the now-available tenant schema
  • Clears registration credentials from metadata after identity creation
  • Idempotent: no-op if identity already exists or no metadata present

Wiring (cmd/meridian/wire_services.go, cmd/meridian/gateway.go):

  • Register self-registered admin hook after all reference data hooks
  • Pass metadata through InitiateTenant gRPC call via protobuf Struct
  • Use Tenant.Status instead of deprecated ProvisioningHint field

Tenant repo (services/tenant/adapters/persistence/repository.go):

  • Add UpdateMetadata method for clearing registration credentials post-provisioning

End-to-end flow

  1. User submits registration form (frontend already exists)
  2. Registration handler creates tenant with provisioning_pending status + admin credentials in metadata
  3. Provisioning worker creates schemas, runs migrations, seeds reference data
  4. Self-registered admin hook creates the admin identity from metadata
  5. Platform admin hook provisions platform admin (existing behavior)
  6. Tenant becomes active
  7. User sees provisioning progress page (PR feat: provisioning progress page for tenants being set up #2124), auto-redirects to login on completion
  8. User logs in with their registered email/password

Test plan

  • Unit tests for async provisioning path (identity NOT created inline)
  • Unit tests for sync provisioning path (identity created inline, backwards compatible)
  • Unit tests for metadata passthrough in loopbackTenantCreator
  • Unit tests for self-registered admin hook metadata key constants
  • All existing registration handler tests updated and passing
  • All existing wiring tests updated and passing
  • Pre-commit hooks pass (gofumpt, golangci-lint, gitleaks)

…ant registration

When a user self-registers and the tenant requires async schema provisioning,
the registration handler now stores the admin email and password hash in
tenant metadata instead of attempting to create the identity immediately
(which would fail because the tenant schema doesn't exist yet).

A new post-provisioning hook reads the stored credentials after schema
provisioning completes and creates the self-registered admin identity with
tenant-owner role, then clears the credentials from metadata.

This completes the end-to-end self-service sign-up flow:
1. User submits registration form
2. Tenant created with provisioning_pending status + credentials in metadata
3. Provisioning worker creates schemas, runs migrations, seeds reference data
4. Self-registered admin hook creates the admin identity
5. Platform admin hook provisions platform admin (existing behavior)
6. Tenant becomes active
7. User sees provisioning progress page, auto-redirects to login on completion
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Tenant creation now accepts registration metadata and returns a result with TenantID and ProvisioningPending. A new post-provisioning hook creates self-registered admin identities from tenant metadata. Tenant metadata read/update support and wiring/tests were added across gateway, handler, bootstrap, and persistence layers.

Changes

Cohort / File(s) Summary
API & Handler
services/api-gateway/registration_handler.go
Created CreateTenantResult; TenantCreator.CreateTenant now accepts metadata map[string]interface{} and returns *CreateTenantResult. Handler hashes password early, passes email/password-hash/verify flag in metadata, conditionally provisions identity inline, and clears metadata after sync provisioning.
Handler Tests
services/api-gateway/registration_handler_test.go
Updated stub TenantCreator to new signature and return type; added tests for async (provisioning-pending) and sync flows, verifying metadata passed and conditional inline identity provisioning.
Gateway Loopback & Wiring
cmd/meridian/gateway.go, cmd/meridian/registration_wiring_test.go, cmd/meridian/wire_services.go
loopbackTenantCreator.CreateTenant signature changed to accept metadata and return *CreateTenantResult; added ClearTenantMetadata(ctx, tenantID). Wiring reuses a single tenantRepo and registers self-registered-admin post-provisioning hook; hook creation errors abort startup. Tests updated to pass metadata and assert result fields and propagated metadata.
Post-provisioning Hook (identity)
services/identity/bootstrap/self_registered_admin.go, services/identity/bootstrap/self_registered_admin_test.go
Added bootstrap package with SelfRegisteredAdminHook and TenantMetadataStore interface. Hook reads _registration_email, _registration_password_hash, and optional _registration_email_verify_required; idempotently creates tenant admin, assigns owner role, clears metadata keys on success. Constructor validates deps; tests added for constructor and constant compatibility.
Tenant Persistence
services/tenant/adapters/persistence/repository.go
Added GetMetadata(ctx, id) and UpdateMetadata(ctx, id, metadata) to read/update tenant JSONB metadata; UpdateMetadata returns ErrTenantNotFound when no rows affected and propagates DB errors.
Gateway Tests (wiring)
cmd/meridian/registration_wiring_test.go
Updated to pass metadata to CreateTenant, assert returned CreateTenantResult fields, and added test ensuring metadata propagates into initiate request.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as Registration Handler
    participant TenantCreator
    participant TenantDB as Tenant DB
    participant ProvisioningWorker as Provisioning Worker
    participant AdminHook as Admin Bootstrap Hook
    participant IdentityDB as Identity DB

    Client->>Handler: Register (email, password)
    Handler->>Handler: Hash password
    Handler->>TenantCreator: CreateTenant(metadata: email, password_hash, verify_flag)
    TenantCreator->>TenantDB: Create tenant with metadata
    TenantDB-->>TenantCreator: CreateTenantResult (TenantID, ProvisioningPending)
    TenantCreator-->>Handler: CreateTenantResult

    alt ProvisioningPending == true
        Handler-->>Client: Return provisioning_pending: true
        Note over ProvisioningWorker: Provisioning runs asynchronously
        ProvisioningWorker->>AdminHook: Run post-provisioning hook for TenantID
        AdminHook->>TenantDB: GetMetadata(TenantID)
        AdminHook->>IdentityDB: Create/activate admin identity (email, password_hash)
        IdentityDB-->>AdminHook: Identity created
        AdminHook->>TenantDB: UpdateMetadata (clear registration keys)
        TenantDB-->>AdminHook: Metadata updated
    else ProvisioningPending == false
        Handler->>IdentityDB: Provision admin inline (email, password_hash)
        IdentityDB-->>Handler: Identity created
        Handler-->>Client: Return provisioning_pending: false
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main objective: implementing a self-service sign-up to tenant creation end-to-end flow, which is the primary focus of the changeset across all modified files.
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset. It explains the motivation, architectural changes, and implementation details across all modified files and provides a clear end-to-end flow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch demo-provisioner-fix--9--self-service-signup

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 4, 2026

Claude Code Review

Commit: c9e59a22 | CI: running (most checks pending)

Summary

Solid implementation of deferred identity creation for async tenant provisioning. The latest commit (c9e59a2) addressed the critical email verification bypass from the previous review - the async path now reads emailVerificationRequired from tenant metadata and creates PENDING_VERIFICATION identities correctly. Metadata handling has been hardened: partial metadata now fails the hook instead of silently succeeding, raw emails removed from logs, and credential-clearing failure is fatal in the async path.

The architecture is clean - TenantMetadataStore interface prevents the cross-service import violation, and the hook integrates well with the existing provisioning worker pattern. Password hashing is done once upfront and reused, which is correct.

Risk Assessment

Area Level Detail
Blast radius Med Registration flow for new tenants; existing tenants unaffected
Rollback Safe No migrations; feature is additive to existing sync path
Scale Low One-time operation per tenant registration
Cross-system Med Spans api-gateway, identity, and tenant services via metadata
Migration N/A No schema changes

Findings

Severity Location Description Status
Improvement self_registered_admin_test.go No unit tests for Provision method (no-op, success, idempotency, error paths) Open

Bot Review Notes

CodeRabbit unresolved threads (5 found):

  1. gateway.go:237 - Silent metadata conversion failure (Critical): Already addressed. Code now returns a fatal error when structpb.NewStruct fails (commit 1f58792).

  2. self_registered_admin.go - Metadata-clearing failure leaves hash (Minor): Already addressed. clearRegistrationMetadata failure is now fatal in the async path (commit 1f58792).

  3. self_registered_admin.go - Partial/malformed metadata (Major): Already addressed. Partial metadata now returns ErrInvalidRegistrationMetadata instead of silently succeeding (commit c9e59a2).

  4. self_registered_admin.go:149 - Weak idempotency on existing email (Major): Valid but low risk. The FindByEmail check skips creation if an identity exists but does not verify it has TENANT_OWNER role. In practice, SaveIdentityWithRoles is transactional, so a partial state (identity exists without role) should not occur. Worth noting but not a blocker since the only path creating identities in a fresh tenant schema is this hook.

  5. self_registered_admin.go - PII in logs (Major): Already addressed. Log messages in the current code use tenant_id only, no raw email addresses (commit c9e59a2).

Previously Flagged

Severity Location Description Status
Critical self_registered_admin.go:159 Async path bypassed email verification Resolved (c9e59a2)
Improvement registration_handler.go:262 Inconsistent credential retention posture (sync=best-effort, async=fatal) Resolved - design is appropriate

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/api-gateway/registration_handler.go (1)

228-262: ⚠️ Potential issue | 🟠 Major

Password hash persists in tenant metadata for sync provisioning path.

In the sync provisioning path (lines 251-262), the handler:

  1. Creates tenant with metadata containing _registration_password_hash (line 235)
  2. Creates identity inline (line 253)
  3. Does NOT clear the metadata

The post-provisioning hook only runs for async provisioning. For sync/active tenants, the password hash remains in tenant metadata indefinitely.

Consider clearing metadata after successful inline identity creation:

🔒️ Proposed fix
 	} else {
 		// Tenant is immediately active - create identity inline.
 		regErr := h.provisionAdminIdentity(ctx, result.TenantID, req.Email, passwordHash)
 		if regErr != nil {
 			// Compensate: delete orphaned tenant to allow the user to retry.
 			if delErr := h.tenantCreator.DeleteTenant(ctx, result.TenantID); delErr != nil {
 				h.logger.ErrorContext(ctx, "registration: failed to compensate (delete tenant)",
 					"tenant_id", result.TenantID, "error", delErr)
 			}
 			return regErr.status, nil, regErr
 		}
+		// Clear registration credentials from tenant metadata (best-effort).
+		// Note: This requires TenantCreator to expose a metadata update method,
+		// or accept that sync-provisioned tenants briefly retain the hash.
 	}

Alternatively, for sync provisioning, skip including credentials in metadata entirely since identity is created inline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/api-gateway/registration_handler.go` around lines 228 - 262, The
tenant metadata currently includes MetaKeyRegistrationPasswordHash when calling
h.tenantCreator.CreateTenant (metadata map), but in the sync provisioning branch
where h.provisionAdminIdentity is called inline the password hash is never
removed; update the flow so credentials are not left in tenant metadata: either
(A) do not add MetaKeyRegistrationPasswordHash to the metadata map for the
immediate-provisioning path (i.e., build metadata without the password hash when
result.ProvisioningPending is false), or (B) after successful
h.provisionAdminIdentity(ctx, result.TenantID, req.Email, passwordHash) call,
call the tenant metadata update/remove API on h.tenantCreator to delete
MetaKeyRegistrationPasswordHash (and handle/log any update errors, compensating
if needed); reference CreateTenant, provisionAdminIdentity, and DeleteTenant to
locate the code to change.
🧹 Nitpick comments (3)
cmd/meridian/wire_services.go (1)

246-255: Duplicate tenant repository instantiation.

Line 249 creates a new tenantpersistence.Repository instance, but line 205 already creates one (repo). While both use the same underlying *gorm.DB connection, creating multiple Repository instances is slightly wasteful.

Consider reusing the existing repo variable:

♻️ Suggested refactor
-	tenantRepo := tenantpersistence.NewRepository(conns.gormDB("tenant"))
-	selfRegHook, hookErr := identitybootstrap.NewSelfRegisteredAdminHook(identityRepo, tenantRepo, logger)
+	selfRegHook, hookErr := identitybootstrap.NewSelfRegisteredAdminHook(identityRepo, repo, logger)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/meridian/wire_services.go` around lines 246 - 255, The code creates a
second tenant repository (tenantRepo := tenantpersistence.NewRepository(...))
even though an existing repo variable was already constructed earlier; replace
the duplicate instantiation by reusing repo when calling
identitybootstrap.NewSelfRegisteredAdminHook (i.e., pass repo instead of
creating tenantRepo) and remove the tenantRepo variable to avoid the redundant
NewRepository call while keeping the call to NewSelfRegisteredAdminHook,
handling hookErr and registering the post-provisioning hook
(selfRegHook.AsPostProvisioningHook()) as before.
services/identity/bootstrap/self_registered_admin_test.go (2)

15-25: Consider adding test for nil tenant repo validation.

The test validates NewSelfRegisteredAdminHook rejects a nil identity repo but doesn't test the nil tenant repo case. The comment at lines 22-24 mentions this limitation.

While constructing a non-nil domain.Repository requires a DB, you could test the nil tenant repo case by passing a non-nil identity repo mock:

💡 Suggestion to improve test coverage
// Mock identity repo for testing nil tenant repo case
type mockIdentityRepo struct {
	identitydomain.Repository
}

func TestNewSelfRegisteredAdminHook_NilTenantRepoRejected(t *testing.T) {
	_, err := NewSelfRegisteredAdminHook(&mockIdentityRepo{}, nil, slog.Default())
	assert.ErrorIs(t, err, ErrNilTenantRepo)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/identity/bootstrap/self_registered_admin_test.go` around lines 15 -
25, Add a unit test that verifies NewSelfRegisteredAdminHook rejects a nil
tenant repository by creating a small mock identity repo (e.g., type
mockIdentityRepo struct { identitydomain.Repository }) and calling
NewSelfRegisteredAdminHook(&mockIdentityRepo{}, nil, slog.Default()), then
assert the returned error is ErrNilTenantRepo; name the test
TestNewSelfRegisteredAdminHook_NilTenantRepoRejected and place it alongside the
existing TestNewSelfRegisteredAdminHook_Validation.

27-37: Placeholder test provides no coverage.

This test only logs a message and doesn't execute any code. Consider either:

  • Removing it (since integration tests cover this)
  • Adding a real unit test with mocks
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/identity/bootstrap/self_registered_admin_test.go` around lines 27 -
37, The placeholder test TestSelfRegisteredAdminHook_NoMetadata_IsNoop provides
no coverage; either delete this test or replace it with a real unit test that
constructs a tenant object with no registration metadata, invokes the Provision
method on the self-registered-admin hook (call Provision on the hook
implementation referenced in your codebase), and assert that Provision returns
nil/no-op and does not modify the tenant or produce side effects; ensure you use
appropriate mocks for any dependencies and assert expected outcomes rather than
only logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/meridian/gateway.go`:
- Around line 226-234: The metadata conversion using
structpb.NewStruct(metadata) can fail and currently only logs a warning
(a.logger.Warn) which leads to creating a tenant with nil req.Metadata and
broken async provisioning; change the handler so that if
structpb.NewStruct(metadata) returns an error you abort registration and return
an error to the caller instead of continuing — i.e., replace the
warn-and-continue behavior around structpb.NewStruct with a failure flow that
returns an appropriate error from the Register handler (so ProvisioningPending
tenants are not created without credentials), keeping the check tied to
structpb.NewStruct, req.Metadata and the ProvisioningPending code path.

In `@services/identity/bootstrap/self_registered_admin.go`:
- Around line 108-118: Currently the handler swallows errors from
h.clearRegistrationMetadata (logged via h.logger.WarnContext) which leaves the
bcrypt password hash in tenant metadata; change the flow so a failure to clear
registration metadata is treated as fatal: return the error from the enclosing
hook (propagate it) instead of continuing to the success log
(h.logger.InfoContext). Ensure you reference and handle failures from
clearRegistrationMetadata( ctx, tenantID, t.Metadata ) by returning that error
(or wrap it) so the caller can retry or abort; if necessary, perform
compensating cleanup of the created identity before returning to avoid
partially-provisioned state.

---

Outside diff comments:
In `@services/api-gateway/registration_handler.go`:
- Around line 228-262: The tenant metadata currently includes
MetaKeyRegistrationPasswordHash when calling h.tenantCreator.CreateTenant
(metadata map), but in the sync provisioning branch where
h.provisionAdminIdentity is called inline the password hash is never removed;
update the flow so credentials are not left in tenant metadata: either (A) do
not add MetaKeyRegistrationPasswordHash to the metadata map for the
immediate-provisioning path (i.e., build metadata without the password hash when
result.ProvisioningPending is false), or (B) after successful
h.provisionAdminIdentity(ctx, result.TenantID, req.Email, passwordHash) call,
call the tenant metadata update/remove API on h.tenantCreator to delete
MetaKeyRegistrationPasswordHash (and handle/log any update errors, compensating
if needed); reference CreateTenant, provisionAdminIdentity, and DeleteTenant to
locate the code to change.

---

Nitpick comments:
In `@cmd/meridian/wire_services.go`:
- Around line 246-255: The code creates a second tenant repository (tenantRepo
:= tenantpersistence.NewRepository(...)) even though an existing repo variable
was already constructed earlier; replace the duplicate instantiation by reusing
repo when calling identitybootstrap.NewSelfRegisteredAdminHook (i.e., pass repo
instead of creating tenantRepo) and remove the tenantRepo variable to avoid the
redundant NewRepository call while keeping the call to
NewSelfRegisteredAdminHook, handling hookErr and registering the
post-provisioning hook (selfRegHook.AsPostProvisioningHook()) as before.

In `@services/identity/bootstrap/self_registered_admin_test.go`:
- Around line 15-25: Add a unit test that verifies NewSelfRegisteredAdminHook
rejects a nil tenant repository by creating a small mock identity repo (e.g.,
type mockIdentityRepo struct { identitydomain.Repository }) and calling
NewSelfRegisteredAdminHook(&mockIdentityRepo{}, nil, slog.Default()), then
assert the returned error is ErrNilTenantRepo; name the test
TestNewSelfRegisteredAdminHook_NilTenantRepoRejected and place it alongside the
existing TestNewSelfRegisteredAdminHook_Validation.
- Around line 27-37: The placeholder test
TestSelfRegisteredAdminHook_NoMetadata_IsNoop provides no coverage; either
delete this test or replace it with a real unit test that constructs a tenant
object with no registration metadata, invokes the Provision method on the
self-registered-admin hook (call Provision on the hook implementation referenced
in your codebase), and assert that Provision returns nil/no-op and does not
modify the tenant or produce side effects; ensure you use appropriate mocks for
any dependencies and assert expected outcomes rather than only logging.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 45a0b3d6-2e67-48ba-927f-eb22ca3bfb2c

📥 Commits

Reviewing files that changed from the base of the PR and between 007371e and 9b63133.

📒 Files selected for processing (8)
  • cmd/meridian/gateway.go
  • cmd/meridian/registration_wiring_test.go
  • cmd/meridian/wire_services.go
  • services/api-gateway/registration_handler.go
  • services/api-gateway/registration_handler_test.go
  • services/identity/bootstrap/self_registered_admin.go
  • services/identity/bootstrap/self_registered_admin_test.go
  • services/tenant/adapters/persistence/repository.go

Comment thread cmd/meridian/gateway.go
Comment thread services/identity/bootstrap/self_registered_admin.go Outdated
claude[bot]
claude Bot previously requested changes Apr 4, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See summary comment for full details. 2 critical findings (credential metadata persistence for sync tenants, duplicate constants), 1 suggestion (non-fatal metadata clear failure).

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 20.80000% with 99 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rvices/identity/bootstrap/self_registered_admin.go 6.32% 74 Missing ⚠️
services/tenant/adapters/persistence/repository.go 0.00% 15 Missing ⚠️
services/api-gateway/registration_handler.go 67.74% 6 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

- Make metadata conversion failure fatal in loopbackTenantCreator
  (prevents silent registration failure for async provisioning)
- Add ClearTenantMetadata to TenantCreator interface and clear
  credentials from tenant metadata after sync identity provisioning
- Make clearRegistrationMetadata failure fatal in post-provisioning
  hook (enforces minimal credential retention)
- Export metadata key constants from bootstrap package and add
  cross-package sync test against gateway constants
- Reuse existing tenant repo in wire_services.go
- Add nil tenant repo validation test
- Remove placeholder test with no coverage
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cmd/meridian/gateway.go (1)

260-269: Silent no-op when tenantRepo is nil may mask configuration issues.

When tenantRepo is nil, ClearTenantMetadata silently returns nil. This could mask a wiring bug where the creator was constructed without a tenant repository. Consider logging a warning or returning an error when this is called with a nil repo, since clearing metadata is an explicit action that should fail visibly if unconfigured.

However, given this is a "chill" review and the sync path calls this after successful identity creation, the impact is limited to leaving credentials in metadata longer than intended (which the async hook would eventually clear anyway).

💡 Optional: Add a warning log when repo is nil
 func (a *loopbackTenantCreator) ClearTenantMetadata(ctx context.Context, tenantID string) error {
 	if a.tenantRepo == nil {
+		a.logger.Warn("ClearTenantMetadata called but tenant repository not configured",
+			"tenant_id", tenantID)
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/meridian/gateway.go` around lines 260 - 269, The method
loopbackTenantCreator.ClearTenantMetadata currently silently returns nil when
its tenantRepo is nil, which can hide wiring/configuration bugs; modify
ClearTenantMetadata to surface this by either returning a descriptive error
(e.g., "tenant repository not configured") or logging a warning before
returning, referencing the tenantRepo field to detect the nil case, and ensure
callers can observe the failure (prefer returning an error if callers expect
clearing to be explicit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/meridian/gateway.go`:
- Around line 260-269: The method loopbackTenantCreator.ClearTenantMetadata
currently silently returns nil when its tenantRepo is nil, which can hide
wiring/configuration bugs; modify ClearTenantMetadata to surface this by either
returning a descriptive error (e.g., "tenant repository not configured") or
logging a warning before returning, referencing the tenantRepo field to detect
the nil case, and ensure callers can observe the failure (prefer returning an
error if callers expect clearing to be explicit).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eb3bfd36-f298-4aa8-b8d7-b1bca781db32

📥 Commits

Reviewing files that changed from the base of the PR and between 9b63133 and 1f58792.

📒 Files selected for processing (6)
  • cmd/meridian/gateway.go
  • cmd/meridian/wire_services.go
  • services/api-gateway/registration_handler.go
  • services/api-gateway/registration_handler_test.go
  • services/identity/bootstrap/self_registered_admin.go
  • services/identity/bootstrap/self_registered_admin_test.go
✅ Files skipped from review due to trivial changes (1)
  • services/identity/bootstrap/self_registered_admin_test.go

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See summary comment. 1 critical, 2 suggestions.

Comment thread services/identity/bootstrap/self_registered_admin.go
Comment thread services/api-gateway/registration_handler.go
Comment thread services/identity/bootstrap/self_registered_admin_test.go
…face

Replace direct dependency on *tenantpersistence.Repository with a
TenantMetadataStore interface defined in the identity bootstrap package.
This fixes the cross-service domain import architecture violation
(identity -> tenant/adapters/persistence).

Add GetMetadata convenience method on tenant repository to satisfy
the new interface without exposing full domain objects.
claude[bot]
claude Bot previously requested changes Apr 4, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See summary comment for full details. One critical finding (email verification bypass on async path) still unaddressed from previous review.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/identity/bootstrap/self_registered_admin.go`:
- Around line 89-109: The hook currently treats missing/invalid
MetaKeyRegistrationEmail or MetaKeyRegistrationPasswordHash as a no-op and
returns nil; instead detect partial or malformed registration metadata in the
handler (the code checking MetaKeyRegistrationEmail,
MetaKeyRegistrationPasswordHash and the variables emailRaw/hashRaw) and either
(a) return a non-nil error to fail provisioning when one or both keys are
present but invalid, or (b) scrub both registration keys from metadata (remove
MetaKeyRegistrationEmail and MetaKeyRegistrationPasswordHash and persist the
cleaned metadata) before returning, so you do not silently leave a password hash
or half-registration state; implement one consistent behavior and update the
code paths around the email/passwordHash type checks and the hook return to
reflect this.
- Around line 133-143: The idempotency check in the self-registered admin flow
uses h.identityRepo.FindByEmail and returns success if any identity exists,
which misses cases where the existing identity lacks RoleTenantOwner or
tenant-owner assignment; instead, when FindByEmail returns an identity, validate
that its role and tenant-owner metadata include RoleTenantOwner and the correct
tenant ID and, if missing or incorrect, update the identity (e.g., via
identityRepo.Update or a helper like ensureTenantOwnerAssignment) to set the
role to RoleTenantOwner and repair tenant owner links before returning nil; add
logging on repair and any update errors and only treat the hook as complete
after successful verification/repair.
- Around line 123-125: The InfoContext calls in self_registered_admin.go are
logging raw user emails (e.g., the call to h.logger.InfoContext with
"tenant_id", tenantID, "email", email); remove the raw email from these logs
(also the similar call at the other block around lines 139-141) and replace it
with a non-PII form such as a redacted value or a one-way hash (e.g., compute a
sha256 hex or mask the local part) and log that under a descriptive key like
"email_hash" or "email_redacted" while keeping "tenant_id" unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8314d83-74a9-44ed-b92d-8e525876eaaa

📥 Commits

Reviewing files that changed from the base of the PR and between 1f58792 and e09b710.

📒 Files selected for processing (3)
  • services/identity/bootstrap/self_registered_admin.go
  • services/identity/bootstrap/self_registered_admin_test.go
  • services/tenant/adapters/persistence/repository.go
✅ Files skipped from review due to trivial changes (1)
  • services/identity/bootstrap/self_registered_admin_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/tenant/adapters/persistence/repository.go

Comment thread services/identity/bootstrap/self_registered_admin.go Outdated
Comment thread services/identity/bootstrap/self_registered_admin.go Outdated
Comment thread services/identity/bootstrap/self_registered_admin.go
- Store emailVerificationRequired in tenant metadata so the
  post-provisioning hook creates PENDING_VERIFICATION identities
  when email verification is enabled (fixes bypass on async path)
- Fail hook on partial/malformed registration metadata instead of
  silently succeeding (prevents orphaned tenants without admin)
- Remove raw email addresses from hook log messages (PII)
- Add MetaKeyRegistrationEmailVerifyRequired constant with sync test
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous critical finding (email verification bypass) addressed in c9e59a2. One improvement suggestion: add Provision method tests. See summary comment.

Comment thread services/identity/bootstrap/self_registered_admin_test.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
services/identity/bootstrap/self_registered_admin.go (1)

140-149: ⚠️ Potential issue | 🟡 Minor

Idempotency check does not verify the tenant-owner role exists.

When an identity with the same email already exists (line 145), the hook skips creation and returns success without verifying that RoleTenantOwner is assigned. In scenarios where the identity was created via other means (manual provisioning, previous failed attempts that created identity but failed before role assignment), the tenant could be left without an owner.

The codebase uses role reconciliation elsewhere (see bootstrap.go for the admin provisioning pattern): when an identity already exists, check for missing required roles and persist them via SaveRoleAssignments. Consider adopting the same pattern here to ensure idempotency handles partial-state scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/identity/bootstrap/self_registered_admin.go` around lines 140 - 149,
The idempotency branch that returns early when an existing identity is found
(using h.identityRepo.FindByEmail) must also ensure the identity has
RoleTenantOwner assigned; instead of immediately returning when existing != nil,
load the current role assignments for that identity and, if RoleTenantOwner is
missing, call the repository method used elsewhere (SaveRoleAssignments) to add
the tenant-owner role for tenantID, logging actions via h.logger and preserving
existing roles; reuse the same reconciliation pattern as in bootstrap.go for
admin provisioning to persist missing roles rather than skipping creation
outright.
🧹 Nitpick comments (2)
services/api-gateway/registration_handler.go (2)

203-210: Duplication of metadata key constants.

These constants are defined identically in services/identity/bootstrap/self_registered_admin.go (lines 32-36). While the comment mentions a test for sync, consider defining them in a shared location (e.g., a shared/registration package) to eliminate duplication and the need for sync tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/api-gateway/registration_handler.go` around lines 203 - 210, The
three metadata constants (MetaKeyRegistrationEmail,
MetaKeyRegistrationPasswordHash, MetaKeyRegistrationEmailVerifyRequired) are
duplicated; extract them into a single shared package (for example a new package
like shared/registration) and remove the duplicate definitions in
registration_handler.go and
services/identity/bootstrap/self_registered_admin.go; then import that package
from both locations and reference the centralized constants (preserve the exact
constant names to avoid downstream changes). Ensure the new package contains the
same const block and update any imports/usages to use the shared package
identifier.

225-230: Error message is misleading for password hash failure.

The error returned is errIdentityCreationFailed, but the actual failure is password hashing. Consider a more specific error or log message for clarity during debugging.

💡 Suggested improvement
 	passwordHash, err := credentials.HashPassword(req.Password)
 	if err != nil {
-		h.logger.ErrorContext(ctx, "registration: failed to hash password", "error", err)
-		return http.StatusInternalServerError, nil, errIdentityCreationFailed
+		h.logger.ErrorContext(ctx, "registration: failed to hash password", "error", err)
+		return http.StatusInternalServerError, nil, fmt.Errorf("failed to prepare credentials: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/api-gateway/registration_handler.go` around lines 225 - 230, The
returned error on password hashing failure is misleading: when
credentials.HashPassword(req.Password) fails in the registration flow (see
passwordHash and credentials.HashPassword), replace the generic
errIdentityCreationFailed with a specific error (e.g., errPasswordHashFailed)
and update the returned HTTP status if needed; keep or refine the
h.logger.ErrorContext call's message to explicitly mention password hashing
failure and include the error details so callers and logs clearly indicate the
actual cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@services/identity/bootstrap/self_registered_admin.go`:
- Around line 140-149: The idempotency branch that returns early when an
existing identity is found (using h.identityRepo.FindByEmail) must also ensure
the identity has RoleTenantOwner assigned; instead of immediately returning when
existing != nil, load the current role assignments for that identity and, if
RoleTenantOwner is missing, call the repository method used elsewhere
(SaveRoleAssignments) to add the tenant-owner role for tenantID, logging actions
via h.logger and preserving existing roles; reuse the same reconciliation
pattern as in bootstrap.go for admin provisioning to persist missing roles
rather than skipping creation outright.

---

Nitpick comments:
In `@services/api-gateway/registration_handler.go`:
- Around line 203-210: The three metadata constants (MetaKeyRegistrationEmail,
MetaKeyRegistrationPasswordHash, MetaKeyRegistrationEmailVerifyRequired) are
duplicated; extract them into a single shared package (for example a new package
like shared/registration) and remove the duplicate definitions in
registration_handler.go and
services/identity/bootstrap/self_registered_admin.go; then import that package
from both locations and reference the centralized constants (preserve the exact
constant names to avoid downstream changes). Ensure the new package contains the
same const block and update any imports/usages to use the shared package
identifier.
- Around line 225-230: The returned error on password hashing failure is
misleading: when credentials.HashPassword(req.Password) fails in the
registration flow (see passwordHash and credentials.HashPassword), replace the
generic errIdentityCreationFailed with a specific error (e.g.,
errPasswordHashFailed) and update the returned HTTP status if needed; keep or
refine the h.logger.ErrorContext call's message to explicitly mention password
hashing failure and include the error details so callers and logs clearly
indicate the actual cause.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d521078f-75f4-4067-beb1-524d62a5bd61

📥 Commits

Reviewing files that changed from the base of the PR and between e09b710 and c9e59a2.

📒 Files selected for processing (4)
  • services/api-gateway/registration_handler.go
  • services/api-gateway/registration_handler_test.go
  • services/identity/bootstrap/self_registered_admin.go
  • services/identity/bootstrap/self_registered_admin_test.go
✅ Files skipped from review due to trivial changes (1)
  • services/identity/bootstrap/self_registered_admin_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/api-gateway/registration_handler_test.go

@bjcoombs bjcoombs dismissed stale reviews from coderabbitai[bot], claude[bot], claude[bot], and coderabbitai[bot] April 4, 2026 14:30

Stale review - issues addressed in subsequent commits

@bjcoombs bjcoombs merged commit 08cdb8a into develop Apr 4, 2026
32 of 40 checks passed
@bjcoombs bjcoombs deleted the demo-provisioner-fix--9--self-service-signup branch April 4, 2026 14:32
bjcoombs added a commit that referenced this pull request Apr 4, 2026
Consolidates the database-per-service plus schema-per-tenant topology into
a single reference covering service ownership, tenant isolation mechanism,
reference data replication, and the one cross-tenant access pattern.
Reflects PR #2125 (public removed from search_path) and PR #2126
(self-service signup). Updates ADR-0003 with a pointer to the new doc and
marks the CockroachDB compatibility section as historical. Archives the
completed database-per-service migration runbook.
bjcoombs added a commit that referenced this pull request Apr 4, 2026
* docs: Add Postgres data model reference and tidy stale schema docs

Consolidates the database-per-service plus schema-per-tenant topology into
a single reference covering service ownership, tenant isolation mechanism,
reference data replication, and the one cross-tenant access pattern.
Reflects PR #2125 (public removed from search_path) and PR #2126
(self-service signup). Updates ADR-0003 with a pointer to the new doc and
marks the CockroachDB compatibility section as historical. Archives the
completed database-per-service migration runbook.

* docs: Update links after archiving database-per-service migration runbook

* docs: Fix relative links in archived database-per-service runbook

---------

Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant