Appearance
HocTapAZ IAM/Profile Services Implementation Report
1. Summary
auth-service is not a deployable Go-platform boundary. The target identity boundary is iam-service, and the current repository already uses services/iam-service for authentication, credentials, sessions, roles, permissions, JWT/JWKS, organization access membership, invites, and IAM audit events.
profile-service is the separate education-profile boundary for display data, avatar/contact metadata, and role-specific student, teacher, parent, and staff profiles. It stores IAM account references as logical ids and does not own passwords, JWT signing, refresh tokens, sessions, roles, or permissions.
The /api/auth and /v1/auth paths remain compatibility route namespaces. They must target iam-service; they are not permission to create an auth-service deployment, task pack, OpenAPI service contract, or runtime directory.
2. Legacy Inventory
The source inventory is recorded in docs/microservices/legacy-user-auth-inventory.md.
Legacy source checked:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/auth/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/app-data/**/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/prisma/schema.prisma
Important mapping:
- Legacy
AuthServiceis a NestJS class name, not a deployable service name. - Legacy auth/session flows map to
iam-service. - Legacy profile update/hydration flows map to
profile-service. - Legacy organization metadata belongs to
school-service; IAM owns only access-control membership and token/session claims.
3. Architecture Decision
Use iam-service, not auth-service, because the boundary is broader than login/logout. IAM owns identity and access management:
- accounts and credentials
- sessions and refresh-token rotation
- access-token signing and JWKS publication
- roles, permissions, and permission checks
- organization membership for access control
- invites, login audit logs, and security events
Use profile-service separately because profile fields are education-context data. Profile owns display and role-profile data, while IAM remains the only source for credential, session, role, and permission state.
4. Services Created
IAM service:
services/iam-servicecontracts/openapi/services/iam-service.yamldocs/api/iam-service.mdservices/iam-service/docs/ARCHITECTURE.mdservices/iam-service/docs/API.mdservices/iam-service/docs/DATABASE.md
Profile service:
services/profile-servicecontracts/openapi/services/profile-service.yamldocs/api/profile-service.mdservices/profile-service/docs/ARCHITECTURE.mdservices/profile-service/docs/API.mdservices/profile-service/docs/DATABASE.md
The deprecated services/user-service remains only as a compatibility scaffold. New identity work belongs to iam-service; new profile work belongs to profile-service. Its runtime now emits deprecation/canonical-owner headers on all responses so compatibility callers and service agents can detect the retirement boundary before route removal.
5. Database Migrations
IAM database: hoctapaz_iam_db.
IAM migrations:
services/iam-service/migrations/000001_init.sqlservices/iam-service/migrations/000002_auth_identity.sqlservices/iam-service/migrations/000003_iam_directory.sqlservices/iam-service/migrations/000004_iam_invites_revoke.sqlservices/iam-service/migrations/000005_iam_membership_outbox.sqlservices/iam-service/migrations/000006_iam_password_reset.sqlservices/iam-service/migrations/000007_iam_google_auth.sqlservices/iam-service/migrations/000008_iam_migration_runs.sqlservices/iam-service/migrations/000009_iam_uuid_shadow_columns.sqlservices/iam-service/migrations/000010_iam_uuid_shadow_dual_write.sqlservices/iam-service/migrations/000011_iam_uuid_primary_key_readiness.sqlservices/iam-service/migrations/000012_iam_uuid_foreign_key_constraints.sqlservices/iam-service/migrations/000013_iam_uuid_primary_key_promotion.sql
IAM primary storage includes identities, refresh tokens, sessions, roles, permissions, role permissions, user roles, organizations, organization members, invites, parent-student links, teacher subject scopes, staff permission scopes, login audit logs, and security events. accounts and credentials are compatibility views.
Profile database: hoctapaz_profile_db.
Profile migrations:
services/profile-service/migrations/000001_init.sqlservices/profile-service/migrations/000002_user_profiles.sqlservices/profile-service/migrations/000003_profile_service_boundary.sqlservices/profile-service/migrations/000004_profile_query_indexes.sqlservices/profile-service/migrations/000005_profile_migration_runs.sqlservices/profile-service/migrations/000006_profile_uuid_shadow_columns.sqlservices/profile-service/migrations/000007_profile_uuid_shadow_dual_write.sqlservices/profile-service/migrations/000008_profile_uuid_primary_key_readiness.sqlservices/profile-service/migrations/000009_profile_uuid_foreign_key_constraints.sqlservices/profile-service/migrations/000010_profile_uuid_primary_key_promotion.sql
Profile primary storage includes base profile rows, student profiles, teacher profiles, parent profiles, staff profiles, profile contacts, teacher KYC self-service/admin-review rows, and parent-student read-model rows. profiles and profile_public_views are compatibility views. The query-index migration makes current accountId, display-name, and phone lookup paths explicit. Additive UUID shadow columns, dual-write triggers, primary-key readiness views, owner-local UUID foreign-key constraints, and UUID primary-key promotion migrations exist. Read-only primary-key readiness views now check UUID candidate nulls, duplicates, rollback legacy_text_id coverage, and Profile account_id one-to-one mapping. The UUID foreign-key status views plus make test-iam-profile-uuid-foreign-key-validation validate the owner-local constraints and ON DELETE policies, and make test-iam-profile-uuid-primary-key-promotion proves UUID-backed primary keys while text ids remain unique for string-id APIs and upserts.
6. APIs Implemented
IAM implements:
POST /v1/auth/registerPOST /v1/auth/loginPOST /v1/auth/logoutPOST /v1/auth/refreshPOST /v1/auth/forgot-passwordPOST /v1/auth/reset-passwordGET /v1/auth/google/configPOST /v1/auth/googleGET /v1/auth/mePOST /v1/auth/me/passwordGET /v1/.well-known/jwks.jsonGET /.well-known/jwks.jsonPOST /v1/accountsGET /v1/accounts/{accountId}PATCH /v1/accounts/{accountId}DELETE /v1/accounts/{accountId}GET /v1/rolesPOST /v1/rolesPATCH /v1/roles/{roleId}DELETE /v1/roles/{roleId}GET /v1/permissionsPOST /v1/permissionsPOST /v1/roles/{roleId}/permissionsPOST /v1/accounts/{accountId}/rolesDELETE /v1/accounts/{accountId}/roles/{roleId}GET /v1/accounts/{accountId}/permissions/{permissionCode}/checkGET /v1/organizationsPOST /v1/organizationsGET /v1/organizations/{organizationId}/membersPOST /v1/organizations/{organizationId}/membersDELETE /v1/organizations/{organizationId}/members/{accountId}GET /v1/accounts/{accountId}/parent-student-linksPOST /v1/accounts/{accountId}/parent-student-linksDELETE /v1/accounts/{accountId}/parent-student-links/{linkId}GET /v1/accounts/{accountId}/teacher-subject-scopesPOST /v1/accounts/{accountId}/teacher-subject-scopesDELETE /v1/accounts/{accountId}/teacher-subject-scopes/{scopeId}GET /v1/accounts/{accountId}/staff-permission-scopesPOST /v1/accounts/{accountId}/staff-permission-scopesDELETE /v1/accounts/{accountId}/staff-permission-scopes/{scopeId}POST /v1/invitesPOST /v1/invites/acceptPOST /v1/invites/{inviteId}/revokeGET /v1/admin/auth-settings/googlePATCH /v1/admin/auth-settings/googleGET /v1/audit/loginGET /v1/audit/security-events
The IAM OpenAPI service spec and mirrored contract now define explicit request bodies and success-envelope response schemas for the auth/session routes plus account directory, role, permission, organization membership, access-scope, Google settings, and audit-list management routes.
Profile implements:
POST /v1/profilesGET /v1/profiles/{accountId}PATCH /v1/profiles/{accountId}GET /v1/profiles/{accountId}/publicGET /v1/profiles/{accountId}/contactsPOST /v1/profiles/{accountId}/contactsDELETE /v1/profiles/{accountId}/contacts/{contactId}GET /v1/profiles/{accountId}/childrenPUT /v1/internal/profiles/{accountId}/children/{studentAccountId}DELETE /v1/internal/profiles/{accountId}/children/{studentAccountId}POST /v1/profiles/{accountId}/studentGET /v1/profiles/{accountId}/studentPATCH /v1/profiles/{accountId}/studentPOST /v1/profiles/{accountId}/teacherGET /v1/profiles/{accountId}/teacherPATCH /v1/profiles/{accountId}/teacherPOST /v1/profiles/{accountId}/parentGET /v1/profiles/{accountId}/parentPATCH /v1/profiles/{accountId}/parentPOST /v1/profiles/{accountId}/staffGET /v1/profiles/{accountId}/staffPATCH /v1/profiles/{accountId}/staff- migration compatibility routes under
/v1/users/{userId}/profile*
7. Security Design
IAM stores password hashes with bcrypt. Refresh tokens are never stored raw; IAM stores bcrypt hashes of SHA-256 token digests and consumes refresh tokens once during refresh/logout. Invite tokens are stored only as SHA-256 hashes.
JWT access tokens preserve legacy-compatible claims and include IAM claims such as roles, org_ids, permissions_version, session_id, and token_type. RS256 signing and JWKS are enabled when a private key is configured; local development can use the HMAC secret fallback. The api-gateway auth adapter can verify IAM RS256 tokens through IAM_JWKS_URL/GATEWAY_JWKS_URL and keeps AUTH_JWT_SECRET as the HS256 route-rehearsal fallback.
Both services use request-id middleware, CORS middleware, structured logging, rate-limit placeholder headers, and unified JSON error envelopes.
IAM service-local authorization protects management routes before any default gateway cutover. Public IAM routes are health/ready, JWKS, register, login, refresh, logout, and invite acceptance. GET /v1/auth/me requires a valid IAM access token, while account directory, role, permission, organization membership, access-scope, invite create/revoke, and audit list routes require an ADMIN bearer token. HTTP tests cover unauthenticated 401, non-admin 403, and admin 200 behavior for the management guard.
Runtime security knobs:
JWT_ACCESS_TTL_MINUTEScontrols IAM access-token lifetime.JWT_REFRESH_TTL_DAYScontrols IAM refresh-token lifetime.PASSWORD_HASH_COSTcontrols IAM bcrypt cost.IAM_JWKS_URLpoints profile/gateway integrations at the IAM JWKS endpoint. Gateway uses it for RS256 route auth and falls back toAUTH_JWT_SECRETfor HS256 local rehearsal tokens.
8. Local Development
Local Compose wiring exists in deploy/docker-compose.yml:
iam-serviceon host port8081profile-serviceon host port8082- separate service-owned PostgreSQL databases in the shared local Postgres container
- service-owned IAM/Profile database roles for runtime and migration jobs
postgres-local-datapersistence plus init scripts for service databases and roles- IAM/Profile schema migrations gated before the services start
- IAM/Profile env examples and Compose/K8s manifests include the IAM/Profile tuning variables above.
Service-local env templates:
services/iam-service/.env.exampleservices/profile-service/.env.example
Service Makefiles support:
make runmake testmake migrate-upmake migrate-downmake docker-buildmake lint
Gateway rehearsal route tables:
deploy/gateway/routes.auth-native-example.jsondeploy/gateway/routes.auth-native-localhost-example.jsondeploy/gateway/routes.profile-native-example.jsondeploy/gateway/routes.profile-native-localhost-example.json
The auth-native route tables cover the native IAM session, password change/reset, Google Auth, and admin Google settings carve-outs. They remain non-default proof artifacts until browser/runtime parity and rollback evidence are recorded for each promoted public route. AUTH_ROUTE_LIVE_CONFIRM=auth-native make test-auth-routes-live is the read-only live gateway smoke for the same non-default route table; it checks IAM Google config, unauthenticated /api/auth/me, and a legacy sibling route without creating accounts or promoting the default route table. On 2026-07-09, that live smoke passed against local iam-service on :8081 and api-gateway on :18086 with artifacts under output/qa/auth-route-live-20260709-115133/: Google config returned 200 through auth-google-config/native_read, unauthenticated /api/auth/me returned 401 with AUTH_UNAUTHORIZED through auth-me/native_read, and the sibling profile route stayed on broad auth/legacy_proxy. AUTH_ROUTE_BROWSER_CONFIRM=auth-native make test-auth-routes-browser is the matching browser-originated login page proof; it opens /auth/login, clicks the Google Auth entry, asserts the browser-observed IAM route headers, and keeps the default route table unchanged. AUTH_SESSION_BROWSER_CONFIRM=auth-session make test-auth-routes-session-browser covers the browser-originated IAM session lifecycle for the non-default auth-native route table: register, login, authenticated /api/auth/me, refresh-token rotation, old-refresh rejection, session restoration after refresh, logout, and refresh-after-logout rejection. It writes only redacted response artifacts, so raw passwords, access tokens, and refresh tokens are not persisted. On 2026-07-09, that lifecycle smoke passed against local public-web on :5207, iam-service on :8081, and api-gateway on :18095 with artifacts under output/playwright/auth-route-session-browser-smoke-20260709-100744/. The summary recorded auth-register/native_write, auth-login/native_write, authenticated auth-me/native_read, auth-refresh/native_write, auth-logout/native_write, rejected old refresh reuse, and rejected refresh after logout. On 2026-07-09, that browser smoke passed against local public-web on :5205, iam-service on :8081, and api-gateway on :18087 with artifacts under output/playwright/auth-route-browser-smoke-20260709-115940/: browser preflight completed at the gateway with credential-safe CORS headers, Google config returned 200 through auth-google-config/native_read, unauthenticated /api/auth/me returned 401 through auth-me/native_read, and the sibling profile route stayed on broad auth/legacy_proxy. make test-iam-profile-route-rollback is the static rollback rehearsal for the same IAM/Profile route tables. It validates that exact native auth/profile carve-outs are absent, removed, or legacy-proxied in deploy/gateway/routes.json while broad auth, admin, and fallback routes remain legacy_proxy. On 2026-07-08, the same rehearsal also passed in live mode against a standalone gateway on http://localhost:18085 with deploy/gateway/routes.json and a dummy legacy target; the observed route headers stayed auth/admin with legacy_proxy, proving rollback routing without promoting default routes. The final promotion handoff now lives in docs/agents/service-tasks/iam-profile-default-cutover-proof.md, with the evidence template at docs/qa/iam-profile-default-cutover-proof.md. It requires production-like populated target validation with IAM_PROFILE_LIVE_VALIDATION_RUN_REPORT=/path/to/migrator-run.json and IAM_PROFILE_LIVE_VALIDATION_REQUIRE_RUN_REPORT=1, zero-issue/zero-rejected postload-reconciliation, live gateway proof, browser proof, and rollback proof before any default /api/auth* or profile route is called promoted. The current default candidate boundary is guarded by make test-iam-profile-default-cutover-candidates: only auth-google-config and unauthenticated auth-me are eligible for the next default-route review because they have both live gateway and browser evidence; all other auth, profile, teacher KYC, and admin KYC rehearsals remain explicitly blocked until their remaining route-specific validation, default candidate diff, and rollback evidence exists. The base and teacher profile update rehearsals now have non-default live/browser proof, but they are still blocked from default promotion by the data and candidate-route gates. deploy/gateway/routes.json remains legacy-safe. make test-iam-profile-default-candidate-diff now produces a review-only candidate route table, unified diff, and summary under output/qa/iam-profile-default-candidate-diff-* without editing deploy/gateway/routes.json; the generated route set can only add auth-google-config and auth-me ahead of the broad auth/legacy_proxy fallback. make test-iam-profile-default-candidate-live now wraps the auth route live smoke for a generated candidate table. Local proof captured at output/qa/iam-profile-default-candidate-live-20260709-095210/ used a temporary iam-service on :18092 and api-gateway on :18093; it verified auth-google-config/native_read, auth-me/native_read, the broad auth/legacy_proxy sibling route, and kept defaultRouteTableModified=false. make test-iam-profile-default-candidate-browser wraps the auth route Playwright smoke for that same candidate boundary. It sets AUTH_ROUTE_BROWSER_EXPECT_ROUTE_SET=candidate-readonly, so browser proof can exercise auth-google-config and auth-me through api-gateway without activating login/register/admin/profile/KYC routes in the default candidate table. Local proof captured at output/qa/iam-profile-default-candidate-browser-20260709-095159/ used public-web on :5206, api-gateway on :18093, and the generated candidate table under output/qa/iam-profile-default-candidate-localhost-20260709-095119/; it verified browser-originated auth-google-config/native_read, unauthenticated auth-me/native_read, and the broad auth/legacy_proxy sibling route while keeping defaultRouteTableModified=false. make test-iam-profile-default-candidate-rollback now ties the same prepared candidate table to the rollback rehearsal. It validates that only auth-google-config and auth-me are active in the candidate set, then checks an api-gateway instance running deploy/gateway/routes.json so broad auth, admin, and fallback return to legacy_proxy without editing the default table. Local proof captured at output/qa/iam-profile-default-candidate-rollback-20260709-095948/ used api-gateway on :18094 and recorded route-header rollback evidence for Google config, the profile sibling route, and admin Google settings. make test-iam-profile-default-cutover-proof-bundle now wraps the remaining default-cutover evidence into one artifact tree: populated target validation with required migrator run report, UUID readiness/FK/PK promotion checks, candidate diff, candidate live proof, candidate browser proof, and candidate rollback proof. The bundle summary deliberately records routePromotionApproved=false and defaultRouteTableModified=false; passing it creates a review package, not an automatic default-route promotion. On 2026-07-09, the profile update live gateway smoke passed against local profile-service on :8082 and api-gateway on :18088 with artifacts under output/qa/profile-route-live-20260709-121055/: PATCH /api/auth/me/profile returned 200 through profile-base-update/native_write, PATCH /api/auth/me/teacher-profile returned 200 through profile-teacher-update/native_write, and GET /api/auth/me stayed on broad auth/legacy_proxy. On 2026-07-09, matching browser proof passed from the real /teacher/profile page with artifacts under output/playwright/profile-route-browser-smoke-20260709-121825/, proving the same base and teacher profile route headers through browser-originated gateway requests while the sibling GET /api/auth/me stayed auth/legacy_proxy. Populated target validation, a candidate default diff, and rollback-after-candidate proof still gate any default profile-route promotion.
9. Kubernetes Local Deployment
The original two-service offline/local manifest is:
deployments/k8s/local/iam-profile-local.yaml
Active platform manifests also include:
deploy/k8s/services/iam-service.yamldeploy/k8s/services/profile-service.yamldeploy/k8s/jobs/schema-migrations.yaml
Static offline K8s and Helm both render local Postgres init scripts, postgres-local-data persistence, IAM/Profile service-role DSNs, and schema-migration Jobs that complete before dependent services are deployed. Helm values include both iam-service and profile-service, including IAM token/hash tunables and the profile IAM JWKS URL.
The broader local/offline runtime now enforces service-owned Postgres roles for every DB-backed Go service. Compose runtime services, Compose migration jobs, static K8s service manifests, K8s migration Jobs, and Helm-rendered manifests use hoctapaz_<service> role/database DSNs, and the Postgres init scripts grant schema create privileges to those roles.
10. Tests
Latest verification run:
go test ./services/iam-service/... ./services/profile-service/...make test-auth-routes test-profile-routesmake test-iam-profile-route-rollbackIAM_PROFILE_ROUTE_ROLLBACK_LIVE=1 IAM_PROFILE_ROUTE_ROLLBACK_CONFIRM=default-legacy IAM_PROFILE_ROUTE_ROLLBACK_ARTIFACT_DIR=output/qa/iam-profile-route-rollback-20260709-114911 GATEWAY_BASE_URL=http://localhost:18085 make test-iam-profile-route-rollbackAUTH_ROUTE_LIVE_CONFIRM=auth-native AUTH_ROUTE_LIVE_ARTIFACT_DIR=output/qa/auth-route-live-20260709-115133 GATEWAY_BASE_URL=http://localhost:18086 make test-auth-routes-liveAUTH_ROUTE_LIVE_SELF_TEST=1 make test-auth-routes-liveAUTH_ROUTE_BROWSER_SELF_TEST=1 make test-auth-routes-browserAUTH_SESSION_BROWSER_SELF_TEST=1 make test-auth-routes-session-browserAUTH_SESSION_BROWSER_CONFIRM=auth-session AUTH_SESSION_BROWSER_ARTIFACT_DIR=output/playwright/auth-route-session-browser-smoke-20260709-100744 AUTH_SESSION_BROWSER_HEADLESS=1 WEB_BASE_URL=http://127.0.0.1:5207 GATEWAY_BASE_URL=http://localhost:18095 make test-auth-routes-session-browser- Profile OpenAPI mirror/schema guard in
services/profile-service/internal/http/openapi_contract_test.go - IAM opt-in Postgres repository integration now covers invite workflow plus directory role/permission/organization-membership parity when
IAM_SERVICE_POSTGRES_TEST_DATABASE_URLis set. sh scripts/test/service-task-pack-coverage.shparses the IAM OpenAPI spec and fails if auth/session or management route request/response schemas drift or disappear before route promotion.make test-iam-profile-runtime-proofverifies static/render IAM/Profile migration-before-runtime wiring across Compose, the two-service local K8s manifest, active offline K8s, and Helm: service-owned DSNs, Postgres init/persistence, distroless readychecks, profileIAM_JWKS_URL, local images, and migration jobs.make test-iam-profile-backfill-loaderverifies the no-writemigrator validate --service iam-profile-backfillpreflight: legacy, IAM, and Profile DSNs stay separate; IAM targetshoctapaz_iam_db; Profile targetshoctapaz_profile_db; invalid target DB names fail before any DB connection or write. It also verifiesmigrator dry-run --service iam-profile-backfillemits deterministic JSON owner-database steps, target table groups, restart checkpoints, validation evidence requirements, and a default-route cutover gate without connecting to or writing any database.- The same guard now requires service-owned IAM/Profile
migration_runsschema plusmigration_id_map.migration_run_idevidence columns before any write-side loader is accepted. - Confirmed
migrator run --service iam-profile-backfill --confirmnow reads legacyUser,RefreshToken,AuthProviderSetting,UserAuthProvider,Organization,OrganizationMember,TeacherProfile,StudentProfile,ParentProfile,TeacherKyc, andParentStudentrows insidepgxread-only transactions. The loader upserts base rows into IAMidentities, IAMrefresh_tokens, IAM derivedsessions, IAMauth_provider_settings, IAMuser_auth_providers, IAMroles, IAMpermissions, IAMrole_permissions, IAMuser_roles, IAMorganizations, IAMorganization_members, IAMidentity_memberships, IAMparent_student_links, IAMteacher_subject_scopes, and Profileusers. It also emits an explicit zero-source IAMstaff_permission_scopesreconciliation because legacy has no per-account staff permission scope table, then loads Profileprofile_contactsderived from legacy email/phone fields, Profileteacher_profiles,student_profiles,parent_profiles,teacher_kyc, andparent_students, writes owner-localmigration_id_maprows linked tomigration_runs, and reportsrowsWritten. - The same run now emits postload reconciliation items for each loaded IAM/Profile owner table with
sourceRows,targetRows,sourceHash,targetHash,matched,issueCount, andrejectedRows; it also updates each owner databasemigration_runsrow with source and target digest hashes and marks the owner runcompletedwhen reconciliation passes. It is still a partial loader because production-like populated DB validation, browser proof for promoted routes, final text-id cleanup, and default route promotion remain blocked. IAM_PROFILE_BACKFILL_POPULATED_SELF_TEST=1 make test-iam-profile-backfill-populatedvalidates the populated-smoke script parser/fixture guard. The live fixture command isIAM_PROFILE_BACKFILL_POPULATED_CONFIRM=populated-fixture make test-iam-profile-backfill-populated; it starts isolated Postgres, applies IAM/Profile target migrations, seeds representative legacy auth/profile rows, runs confirmedmigrator run --service iam-profile-backfill --confirm, asserts reconciliation rows including zero-sourcestaff_permission_scopes, and checksiam_uuid_shadow_validationplusprofile_uuid_shadow_validation. The fixture now also asserts completedmigration_runsrows with source/target hash evidence. This proves loader/migration/UUID-shadow compatibility on a hermetic fixture, not production data completeness or default route readiness.- Latest local fixture proof: on 2026-07-09,
IAM_PROFILE_BACKFILL_POPULATED_CONFIRM=populated-fixture IAM_PROFILE_BACKFILL_POPULATED_ARTIFACT_DIR=output/qa/iam-profile-backfill-populated-20260709-113538 make test-iam-profile-backfill-populatedpassed. The capturedbackfill-run.jsonrecorded 4 legacy user rows, IAMrowsWritten=63, ProfilerowsWritten=17, matched identity/profile reconciliation, zero staff permission scope source rows, zero issues/rejected rows for sampled reconciliations, and clean IAM/Profile UUID shadow validation views. IAM_PROFILE_LIVE_VALIDATION_SELF_TEST=1 make test-iam-profile-live-populated-validationvalidates the read-only live populated validation script. Against the same populated fixture,IAM_PROFILE_LIVE_VALIDATION_CONFIRM=live-populatedverified non-empty IAM/Profile base rows, completedmigration_runswith hash evidence, populatedmigration_id_maprows, zero owner-local orphan references, and zero UUID-shadow issues in both owner databases. The script now acceptsIAM_PROFILE_LIVE_VALIDATION_RUN_REPORTplusIAM_PROFILE_LIVE_VALIDATION_REQUIRE_RUN_REPORT=1to validate capturedmigrator run --service iam-profile-backfill --confirmJSON and require allpostload-reconciliationitems to havematched=true,issueCount=0,rejectedRows=0, and source/target hashes. Running that validation against the real populated target DSNs with the captured run report remains the production-like cutover evidence.- Same-target fixture proof was refreshed on 2026-07-09 by keeping the populated fixture target online after backfill and running the live populated validation plus UUID readiness/FK/PK-promotion targets against the same IAM/Profile DSNs. Artifacts are under
output/qa/iam-profile-target-validation-20260709-114607/. The run recorded live validationstatus=passed, IAM/Profile base rows4/4, migration-id map rows63/17, orphan references0/0, 22 matchedpostload-reconciliationitems from the required run report, UUID readiness rows81/39with no blocking issues, UUID FK constraints26/10with no unvalidated constraints, and promoted UUID PK metadata for21/9IAM/Profile tables with zero legacy text FKs. The QA trace is documented in IAM/Profile Target Validation Smoke. IAM_PROFILE_UUID_FK_VALIDATION_SELF_TEST=1 make test-iam-profile-uuid-foreign-key-validationvalidates the UUID foreign-key guard logic. Against an already-migrated target,IAM_PROFILE_UUID_FK_VALIDATION_CONFIRM=uuid-foreign-key-validationvalidates the owner-localNOT VALIDIAM/Profile UUID constraints and checks the FK status views for expected ON DELETE policies. This is a target-table scan and metadata validation gate; primary-key promotion is validated by the dedicated UUID primary-key promotion target.IAM_PROFILE_UUID_PK_PROMOTION_SELF_TEST=1 make test-iam-profile-uuid-primary-key-promotionvalidates the UUID primary-key promotion guard logic. Against an already-migrated target,IAM_PROFILE_UUID_PK_PROMOTION_CONFIRM=uuid-primary-key-promotionproves IAM/Profile service-owned primary keys are backed by UUID columns, text ids remain unique, legacy owner-local text FKs are removed, and UUID FK status views stay clean. This does not promote default public routes or drop text id columns.- Added first additive UUID cutover migrations:
000009_iam_uuid_shadow_columns.sqland000006_profile_uuid_shadow_columns.sql. They add UUID shadow columns, legacy text id preservation, reference UUID backfill, unique/reference indexes, andiam_uuid_shadow_validation/profile_uuid_shadow_validationwithout changing current text primary keys or OpenAPI string ids. - Added UUID shadow dual-write migrations:
000010_iam_uuid_shadow_dual_write.sqland000007_profile_uuid_shadow_dual_write.sql. They add owner-database triggers so repository writes maintainid_uuid,legacy_text_id, Profileusers.account_id, and internal reference UUID columns while public service interfaces keep string ids. IAM audit/security validation now ignores null optionalaccount_idvalues and still flags non-null account references that missaccount_id_uuid. - Repository integration proof now includes
TestPostgresIAMUUIDShadowDualWriteIntegrationandTestPostgresProfileUUIDShadowDualWriteIntegration, each applying all service migrations in an isolated PostgreSQL schema, writing representative repository rows, and asserting the matching UUID shadow validation view has zero issues. Theiam_uuid_primary_key_readinessandprofile_uuid_primary_key_readinessviews plusmake test-iam-profile-uuid-primary-key-readinessnow provide the read-only UUID PK candidate gate. Theiam_uuid_foreign_key_constraintsandprofile_uuid_foreign_key_constraintsviews plusmake test-iam-profile-uuid-foreign-key-validationvalidate the owner-local UUID FK constraint set. Theiam_uuid_primary_key_constraintsandprofile_uuid_primary_key_constraintsviews plusmake test-iam-profile-uuid-primary-key-promotionprove the promoted UUID PK metadata. Production-like populated DB validation, browser proof for promoted routes, final text-id cleanup, and default-route promotion remain pending. - Temporary Postgres schema proof applied all IAM/Profile migrations through those additive UUID migrations and queried the shadow/readiness/FK status views successfully; this proves SQL syntax and ordering on clean empty service databases, not production data completeness or default route readiness.
IAM_PROFILE_CLEAN_COMPOSE_SELF_TEST=1 make test-iam-profile-clean-composevalidates the live-smoke script fixtures. The live proof command isIAM_PROFILE_CLEAN_COMPOSE_CONFIRM=clean-runtime make test-iam-profile-clean-compose; it starts an isolated clean Compose IAM/Profile slice, verifies migration success,schema_migrationsrows for each IAM/Profile SQL file, Postgres health, service/healthz//readyz, and IAM JWKS, then tears the project down with volumes unlessIAM_PROFILE_CLEAN_COMPOSE_KEEP=1is set. It uses default host ports5433,8081, and8082, or theIAM_PROFILE_HOST_POSTGRES_PORT,IAM_PROFILE_HOST_IAM_PORT, andIAM_PROFILE_HOST_PROFILE_PORToverrides for occupied developer machines. When IAM/Profile service images already exist locally, setIAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1to useIAM_PROFILE_IAM_IMAGE,IAM_PROFILE_PROFILE_IMAGE, and a generatedIAM_PROFILE_MIGRATOR_IMAGEwith Docker Compose--no-build; setIAM_PROFILE_REBUILD_MIGRATOR_IMAGE=1after migration changes.IAM_PROFILE_K8S_SELF_TEST=1 make test-iam-profile-local-k8svalidates the local-K8s smoke script render path. The live proof command isIAM_PROFILE_K8S_LIVE_CONFIRM=local-k8s make test-iam-profile-local-k8s; it applies the two-service local IAM/Profile manifest into an isolated namespace, runs temporary IAM/Profile schema migration Jobs withhoctapaz/migrator:local, checksschema_migrationsinsideiam-dbandprofile-db, restarts the IAM/Profile deployments, verifies/healthz,/readyz, and IAM JWKS throughkubectl port-forward, and deletes the namespace by default. This is available as the local/offline K8s runtime proof path after service and migrator images are built and loaded into the cluster. It refuses non-local contexts by default unlessIAM_PROFILE_K8S_ALLOW_REMOTE=1is set deliberately.- Local K8s live proof was refreshed on 2026-07-08 after the IAM 13-file/Profile 10-file readiness/FK/PK-promotion migrations against
kind-hoctapaz-go. Rebuilthoctapaz/iam-service:local,hoctapaz/profile-service:local, andhoctapaz/migrator:localwere loaded into the kind node, thenIAM_PROFILE_K8S_LIVE_CONFIRM=local-k8s IAM_PROFILE_K8S_NAMESPACE=hoctapaz-iam-profile-uuidpk2-20260708 IAM_PROFILE_K8S_TIMEOUT=300s IAM_PROFILE_K8S_IAM_PORT=58191 IAM_PROFILE_K8S_PROFILE_PORT=58192 make test-iam-profile-local-k8sran through a temporary kubeconfig set tokind-hoctapaz-go. It applied the isolated namespace, completedmigrate-iam-serviceandmigrate-profile-service, verified service-ownedschema_migrationscounts for the current IAM/Profile migration sets, rolled IAM/Profile deployments, checked/healthz,/readyz, and IAM JWKS through port-forward, and removed the smoke namespace. - Clean Compose live proof was refreshed on 2026-07-08 after the IAM 13-file/Profile 10-file readiness/FK/PK-promotion migrations with
IAM_PROFILE_CLEAN_COMPOSE_CONFIRM=clean-runtime,IAM_PROFILE_HOST_POSTGRES_PORT=55433,IAM_PROFILE_HOST_IAM_PORT=58081, andIAM_PROFILE_HOST_PROFILE_PORT=58082. It rebuilt IAM/Profile/migrator images for Compose projecthoctapaz-iam-profile-smoke, verified IAM/Profile migration completion and per-fileschema_migrationsrows for the current IAM/Profile migration sets, Postgres health, service/healthz//readyz, and IAM JWKS, then removed the isolated project and volumes. - Clean Compose live proof was refreshed again on 2026-07-09 with local images and Docker Compose
--no-buildbecause the build-mode path was waiting on externalgolang:1.22image metadata. The passing run usedIAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1,hoctapaz/iam-service:local,hoctapaz/profile-service:local,hoctapaz/migrator:local, host ports55435,18081, and18082, and wrote artifacts underoutput/qa/iam-profile-clean-compose-20260709-114124/. It verified IAM/Profile migration containers exited0, service/healthz//readyz, and IAM JWKS. The QA trace is documented in IAM/Profile Clean Compose Smoke. make test-runtime-foundationpnpm docs:buildgit diff --check -- <touched runtime/docs/scripts files>
11. Migration Notes From Legacy Source
Default public /api/auth/* routing remains conservative. Native auth and profile route tables are rehearsal artifacts until response-shape parity, browser/runtime evidence, production-like backfill validation, and explicit promotion approval are complete. Current default-table rollback notes are captured in docs/qa/iam-profile-route-rollback-rehearsal.md.
Do not reintroduce a deployable auth-service. If legacy docs mention AuthService, treat it as the NestJS class name and map the Go-platform runtime boundary to iam-service.
12. Remaining Risks / TODO
- Current migrations keep
TEXTid columns for migration compatibility and public string-id APIs, while service-owned PostgreSQL primary keys are promoted to UUID columns in the target IAM/Profile schemas. Production-like target validation, final text-id cleanup, and public route/browser promotion remain separate cutover work tracked in IAM/Profile UUID Cutover Design. - Accounts/credentials and profile public views are compatibility views over service-owned tables, not separate physical source-of-truth tables.
- IAM directory/audit APIs are intended for gateway/internal use. Service-local admin guards are in place for bearer-token management routes, and owner/ service-to-service routes now require
X-Internal-Serviceplus the sharedX-Internal-Token; gateway route rehearsal and browser/runtime proof remain required before direct public exposure. - Profile teacher KYC self submit/read and owner-side admin list/detail/approve/reject are implemented as profile-owned storage with safe self-service redaction, admin-only review metadata, and audit-shaped submit/review events. Non-default gateway compatibility rehearsals now cover exact self-service and admin KYC paths; default promotion, admin-service wrappers, and profile-owned parent-student read-model/UI workflows still need cutover evidence before public promotion. Profile contacts, Profile parent-child projections, and IAM parent-student permission links are implemented as separate profile metadata, read-model, and access-control APIs.
- Profile stores IAM-synchronized role/status metadata for compatibility. It must remain projection data, not permission source-of-truth.
- IAM and
school-serviceorganization-membership ownership must stay explicit to avoid split-brain access decisions. - Clean service-database migration/integration proof now exists for the IAM/Profile Compose and local K8s slices. IAM has opt-in repository parity coverage for invites, directory permissions, organization membership, and membership outbox behavior. Static/render runtime wiring is covered by
make test-iam-profile-runtime-proof, clean Compose live proof is captured bytest-iam-profile-clean-composewith latest local-image artifacts underoutput/qa/iam-profile-clean-compose-20260709-114124/, and local K8s live proof is captured bytest-iam-profile-local-k8s. Current default-route rollback proof is captured bytest-iam-profile-route-rollback, with latest live route-header artifacts underoutput/qa/iam-profile-route-rollback-20260709-114911/. Non-default browser proof now exists for the read-only auth candidates and the base/teacher profile update routes. Production-like populated DB validation, route-promotion candidate diffs, final text-id cleanup, and default route promotion remain pending. - Legacy user backfill into IAM/Profile databases must run with validation reports before any default
/api/auth/*cutover beyond rehearsed routes. The no-write DSN preflight, dry-run plan, target run-state schema, confirmedmigration_runscheckpoint writer, base legacyUserrow loader, IAM refresh-token/session/provider row loaders, IAM role/user-role, permission-catalog/role-permission, and organization/member projection loaders, IAM parent-student access-link loader, IAM teacher subject-scope loader, explicit zero-source staff permission scope reconciliation, plus Profile contact, education-profile, KYC, and parent-child read-model loaders exist. Postload source/target count and hash reconciliation is emitted for the loaded tables. Additive UUID shadow migrations, dual-write triggers, primary-key readiness views, owner-local UUID foreign-key validation constraints, and UUID primary-key promotion now exist for IAM/Profile, but production-like populated DB validation, route-promotion candidate diffs, final text-id cleanup, and default route promotion remain pending. The latest same-target fixture validation artifacts are underoutput/qa/iam-profile-target-validation-20260709-114607/.