Skip to content

Commit 817ec45

Browse files
committed
refactor: Move demo user seeding to post-provisioning hook
SeedDemoUsers ran at startup (wire_services.go:102) before tenant schemas were provisioned. On deploys that drop and recreate schemas, the provisioning worker takes 30-60s after boot to apply migrations, so the startup seeder consistently failed with "relation identity does not exist" and the demo user was never created. Move demo user seeding to a post-provisioning hook registered as "demo-operator" in the provisioning worker's hook chain. This guarantees the identity schema exists before the seeder runs - the hook fires after all service migrations complete for each tenant. The hook reads DEMO_OPERATOR_EMAIL, DEMO_OPERATOR_PASSWORD, and DEMO_OPERATOR_TENANT at registration time and builds a tenant lookup set. For each provisioned tenant, it checks membership in the set and seeds the demo user only for matching tenants. If the env vars are not set, the hook is a silent no-op (safe for production). SeedDemoUsers remains exported for tests and manual invocation but is no longer called from the startup path.
1 parent 28e0e04 commit 817ec45

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

cmd/meridian/wire_services.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,10 @@ func registerServices(
9797
logger.Warn("identity bootstrap failed, service startup continues", "error", err)
9898
}
9999

100-
// Seed demo users (operator, etc.) from environment variables.
101-
// No-op if env vars are not set. Idempotent on every boot.
102-
if err := identitybootstrap.SeedDemoUsers(ctx, identityRepo); err != nil {
103-
logger.Warn("demo user seeding failed, service startup continues", "error", err)
104-
}
100+
// Demo user seeding moved to post-provisioning hook ("demo-operator")
101+
// registered in startProvisioningWorker. This ensures demo users are
102+
// seeded after tenant schemas exist, not at startup when schemas may
103+
// not yet be provisioned.
105104

106105
// Tier 1: Depend on Tier 0 via loopback
107106
for _, wire := range []struct {
@@ -195,6 +194,8 @@ func createSchemaProvisioner(baseDSN string, platformDB *gorm.DB, logger *slog.L
195194
// 3. saga-definitions: Seeds platform default saga scripts into tenant schema
196195
// 4. account-type-blueprints: Seeds canonical account type blueprints
197196
// 5. valuation-defaults: Seeds system valuation methods and policies
197+
// 6. self-registered-admin: Creates admin identity from registration metadata
198+
// 7. demo-operator: Seeds demo user from DEMO_OPERATOR_* env vars (no-op in production)
198199
//
199200
// All hooks are fail-hard: any failure prevents tenant activation.
200201
func startProvisioningWorker(ctx context.Context, prov *tenantprovisioner.PostgresProvisioner, conns *serviceConns, logger *slog.Logger) (*tenantworker.ProvisioningWorker, func(), error) {
@@ -253,6 +254,11 @@ func startProvisioningWorker(ctx context.Context, prov *tenantprovisioner.Postgr
253254
}
254255
w.RegisterPostProvisioningHook("self-registered-admin", selfRegHook.AsPostProvisioningHook())
255256

257+
// Demo operator: seeds demo user identities configured via DEMO_OPERATOR_*
258+
// env vars. No-op if env vars are not set (safe for production). Must run
259+
// after admin-identity so the identity schema is guaranteed to exist.
260+
w.RegisterPostProvisioningHook("demo-operator", identitybootstrap.AsDemoUserHook(identityRepo))
261+
256262
go w.Start(ctx)
257263
logger.Info("provisioning worker started")
258264

services/identity/bootstrap/bootstrap.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,35 @@ func buildRoleAssignments(tid tenant.TenantID, identityID uuid.UUID, roles []aut
415415
}
416416
return assignments
417417
}
418+
419+
// AsDemoUserHook returns a post-provisioning hook that seeds demo operator
420+
// identities into newly provisioned tenant schemas.
421+
//
422+
// The hook reads demo user configuration from environment variables
423+
// (DEMO_OPERATOR_EMAIL, DEMO_OPERATOR_PASSWORD, DEMO_OPERATOR_TENANT).
424+
// If the env vars are not set, the hook is a silent no-op - safe for
425+
// production. For each provisioned tenant, the hook checks whether the
426+
// tenant is in the configured DEMO_OPERATOR_TENANT list and seeds the
427+
// demo user only for matching tenants.
428+
//
429+
// Usage:
430+
//
431+
// repo := persistence.NewRepository(db)
432+
// worker.RegisterPostProvisioningHook("demo-operator", bootstrap.AsDemoUserHook(repo))
433+
func AsDemoUserHook(repo domain.Repository) func(ctx context.Context, tenantID tenant.TenantID) error {
434+
// Load config once at registration time, not per invocation.
435+
users := loadDemoUsers()
436+
// Build a lookup set of tenant IDs that need demo users.
437+
tenantSet := make(map[string]DemoUser, len(users))
438+
for _, u := range users {
439+
tenantSet[u.TenantID] = u
440+
}
441+
442+
return func(ctx context.Context, tenantID tenant.TenantID) error {
443+
u, ok := tenantSet[string(tenantID)]
444+
if !ok {
445+
return nil // this tenant is not configured for demo users
446+
}
447+
return seedDemoUser(ctx, repo, u)
448+
}
449+
}

0 commit comments

Comments
 (0)