Appearance
IAM/Profile Cutover Hardening Task
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: orchestrator
Owner: orchestrator with iam-service, profile-service, api-gateway, and migration owners.
Problem:
The native IAM/Profile foundation exists, but pasted architecture requirements still have cutover-grade gaps: production-like populated target validation, default-route proof, browser proof, final text-id cleanup, and service-local security hardening. These must be handled explicitly before claiming the auth-service to iam-service transition is production-ready.
Goal:
Turn the remaining IAM/Profile cutover risks into small, verifiable slices without reintroducing auth-service, broad user-service, cross-service DB reads, or frontend calls to service hostnames.
Legacy source evidence:
/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- frontend auth/profile callers under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/web.
Writable files:
services/iam-service/**services/profile-service/**services/api-gateway/**only for non-default rehearsal/cutover routescontracts/openapi/services/iam-service.yamlcontracts/openapi/services/profile-service.yamlservices/iam-service/api/openapi.yamlservices/profile-service/api/openapi.yamldocs/api/iam-service.mddocs/api/profile-service.mddocs/microservices/iam-profile-*.mddocs/migration/**docs/agents/service-tasks/iam-profile-cutover-hardening.md
Orchestrator-owned files:
- Default gateway route table, root Makefile, Compose, Helm, K8s manifests, migration job lists, OpenAPI index pages, VitePress navigation, and final
services/user-serviceretirement/removal.
Tasks:
- Completed first profile indexing slice:
000004_profile_query_indexes.sqladds explicit account-id, display-name, and phone lookup indexes for current text-compatible API storage. Newly generated IAM/Profile ids are UUIDv4 strings, and the promoted target schemas use UUID-backed PostgreSQL primary keys while preserving text ids for route/API compatibility. - Completed first IAM service-local management guard slice: account directory, role, permission, organization membership, access-scope, invite create/revoke, and audit list routes now require an
ADMINbearer token iniam-serviceitself. HTTP regression coverage proves unauthenticated401, non-admin403, and admin200behavior while public auth/JWKS/invite-accept routes remain open as designed. - Completed first IAM account/profile boundary guard:
PATCH /v1/accounts/{id}has an explicit OpenAPI request body and HTTP regression coverage proving profile-owned fields such asavatarUrlare rejected instead of becoming IAM account data.fullNameremains documented as temporary legacy session compatibility material until profile/gateway cutover. - Completed first IAM password reset/change parity slice: native
POST /v1/auth/me/password,POST /v1/auth/forgot-password, andPOST /v1/auth/reset-passwordnow cover current-password validation, neutral forgot responses, one active hashed reset token per account, single-use reset, refresh/session revocation, and audit/security events without raw credential material. Default/api/auth/*route promotion remains pending gateway proof. - Completed first IAM Google provider-link parity slice: native
GET /v1/auth/google/config,POST /v1/auth/google, and admin-onlyGET/PATCH /v1/admin/auth-settings/googlenow cover env/DB settings precedence, Google ID-token verification, allowed-domain policy, existing provider login, email auto-link, register-mode account creation, provider-link storage, and audit/security events without raw Google credentials or client id. Non-default gateway route-table rehearsal now covers/api/auth/google*,/api/auth/forgot-password,/api/auth/reset-password,/api/auth/me/password, and/api/admin/auth-settings/google; default route promotion and browser proof remain pending. - Completed first Profile OpenAPI parity slice: service and mirrored contract schemas now cover full profile, public profile, base update, compatibility profile routes, student/teacher/parent/staff profile request/response envelopes, staff display metadata, and redacted
TeacherKycSafe. A static profile-service test keeps the mirror and required schemas from regressing. - Completed first IAM/Profile deploy-bootstrap slice: local Compose now persists Postgres data, runs IAM/Profile schema migrations before the services start, uses
/readyzhealthchecks for distroless service images, and connects IAM/Profile runtime plus migrations through service-owned database roles. Static offline K8s and Helm now render Postgres init scripts, apostgres-local-dataPVC, IAM/Profile role DSNs, and migration completion waits. Live clean-cluster runtime proof is now covered by the clean Compose and local kind smoke runs below. - Completed named IAM/Profile runtime proof guard:
make test-iam-profile-runtime-proofchecks the Compose IAM/Profile migration-before-service dependency, service-owned DSNs, distroless readychecks, profileIAM_JWKS_URL, K8s offline migration/service manifests, Postgres init/persistence, and Helm-rendered IAM/Profile migration/runtime wiring. This is static/render proof only; live runtime proof is covered by the opt-in clean Compose and local K8s smokes below. - Added opt-in live clean Compose smoke target:
make test-iam-profile-clean-composerunsscripts/test/iam-profile-clean-compose-smoke.sh. It requiresIAM_PROFILE_CLEAN_COMPOSE_CONFIRM=clean-runtime, creates an isolated Compose project, starts only Postgres, IAM/Profile migrations,iam-service, andprofile-service, verifies migration container success,schema_migrationsrows for each IAM/Profile SQL file, Postgres health,/healthz,/readyz, and IAM JWKS over real HTTP, then tears the project down with volumes by default. The live smoke preflights host ports5433,8081, and8082by default and supportsIAM_PROFILE_HOST_POSTGRES_PORT,IAM_PROFILE_HOST_IAM_PORT, andIAM_PROFILE_HOST_PROFILE_PORToverrides; do not stop another developer stack just to run it. If service images are already present 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=1when migration files changed. A self-test path exists for CI syntax/fixture coverage. Running this target successfully is the clean Compose runtime proof. - Added opt-in local/offline Kubernetes smoke target:
make test-iam-profile-local-k8srunsscripts/test/iam-profile-local-k8s-smoke.sh. It requiresIAM_PROFILE_K8S_LIVE_CONFIRM=local-k8s, applies the two-service local IAM/Profile manifest into an isolated namespace, runs temporarymigrate-iam-serviceandmigrate-profile-serviceJobs with the local migrator image, verifiesschema_migrationsrows inside each service-owned Postgres StatefulSet, restarts IAM/Profile deployments, checks/healthz,/readyz, and IAM JWKS throughkubectl port-forward, then deletes the namespace by default. This is the live local K8s runtime proof path; it still has to be run against a real local cluster with images already loaded before default route cutover can use it as evidence. It refuses non-local contexts unlessIAM_PROFILE_K8S_ALLOW_REMOTE=1is set deliberately. - Local K8s live proof refreshed on 2026-07-08 after the IAM 13-file/Profile 10-file readiness/FK/PK-promotion migrations with
kind-hoctapaz-go, rebuilt and loadedhoctapaz/iam-service:local,hoctapaz/profile-service:local, andhoctapaz/migrator:local, 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-k8sthrough a temporary kubeconfig set tokind-hoctapaz-go. It verified IAM/Profile migration Jobs, service-ownedschema_migrationscounts for the current IAM/Profile migration sets, service/healthz//readyz, IAM JWKS overkubectl port-forward, and namespace cleanup. - Clean Compose live proof 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 migration container completion and per-fileschema_migrationsrows for the current IAM/Profile migration sets, Postgres health, service/healthz//readyz, and IAM JWKS over HTTP, then cleaned up the isolated project. - Clean Compose live proof refreshed again on 2026-07-09 with
IAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1,hoctapaz/iam-service:local,hoctapaz/profile-service:local,hoctapaz/migrator:local, host ports55435,18081, and18082, and artifacts underoutput/qa/iam-profile-clean-compose-20260709-114124/. Migration containers exited0, IAM/Profile/healthzand/readyzreturned healthy responses, and IAM JWKS returned compatibility metadata. This local-image path avoided a build-mode wait on externalgolang:1.22image metadata. - Clean Compose build-mode proof refreshed on 2026-07-10 with an isolated project
hoctapaz-iam-profile-clean-20260710, host ports5543,18081, and18082, and artifacts underoutput/qa/iam-profile-clean-compose-20260710-1555/. Docker rebuilt the IAM, Profile, and migrator images, both migration containers exited0, every current IAM/Profile migration was recorded in its service-owned database, both services answered/healthzand/readyz, IAM served JWKS compatibility metadata, and cleanup removed the project, network, and volume. This is still clean-runtime evidence, not populated-target cutover proof. - Completed UUID migration design slice:
docs/microservices/iam-profile-uuid-cutover-design.mddefines additive UUID columns, old text-id compatibility/backfill ids, Profileaccount_idsemantics, OpenAPI string ids, service-to-service event ids, validation queries, and rollback/backfill ordering for UUID primary-key promotion. - Completed first additive UUID shadow-column migration slice:
000009_iam_uuid_shadow_columns.sqland000006_profile_uuid_shadow_columns.sqladdid_uuid,legacy_text_id, reference UUID columns, parse-or-hash backfill, indexes, and owner-local validation views without changing text primary keys or OpenAPI string ids. - Completed UUID shadow dual-write slice:
000010_iam_uuid_shadow_dual_write.sqland000007_profile_uuid_shadow_dual_write.sqladd owner-DB triggers that keep UUID shadow ids and reference UUID columns populated for repository writes while the service interfaces continue to use string ids. The IAM trigger also keeps nullable audit/security log account references from producing false validation issues. - Completed UUID primary-key readiness guard slice:
000011_iam_uuid_primary_key_readiness.sqland000008_profile_uuid_primary_key_readiness.sqladd read-only readiness views that check every owner-local UUID primary-key candidate for nulls, duplicates, and rollbacklegacy_text_idcoverage.make test-iam-profile-uuid-primary-key-readinessis the opt-in read-only live gate for already-migrated IAM/Profile target databases; its self-test is part of runtime-foundation coverage. This does not swap primary keys. - Completed UUID foreign-key validation guard slice:
000012_iam_uuid_foreign_key_constraints.sqland000009_profile_uuid_foreign_key_constraints.sqladd owner-local UUID foreign keys asNOT VALIDconstraints plus status views.make test-iam-profile-uuid-foreign-key-validationvalidates those constraints on already-migrated IAM/Profile target databases and checks ON DELETE policy metadata; it scans tables and updates constraint metadata only. This does not swap primary keys or promote default routes. - Completed UUID primary-key promotion slice:
000013_iam_uuid_primary_key_promotion.sqland000010_profile_uuid_primary_key_promotion.sqlvalidate the owner-local UUID FKs, keep public text ids unique for string-id APIs and backfill upserts, move service-owned primary-key constraints toid_uuidor UUID composite keys, and exposeiam_uuid_primary_key_constraints/profile_uuid_primary_key_constraintsfor read-only validation.make test-iam-profile-uuid-primary-key-promotionproves the migrated target DB metadata. This does not promote default public routes or drop text id columns. - Promote default public routes only after production-like populated validation reports, clean UUID readiness/FK/promotion output on target DSNs, browser proof, and rollback evidence.
- Use
docs/agents/service-tasks/iam-profile-default-cutover-proof.mdfor the final route-promotion evidence bundle. This cutover hardening pack tracks the blockers; the default cutover proof pack owns candidate route lists, production-like run-report validation, gateway live proof, browser proof, and rollback artifacts. - For IAM, remove the temporary
full_name/legacy account envelope material only after profile/gateway cutover evidence exists. - For Profile, model
account_idas the explicit logical IAM reference in the storage/API contract, add requiredaccount_id,display_name, andphoneindexes, and keep cross-database IAM foreign keys out. - Build legacy user backfill tooling that writes only to
hoctapaz_iam_dbandhoctapaz_profile_db, emits count/hash validation reports, and leaves the legacy database read-only. Usedocs/agents/service-tasks/iam-profile-backfill-loader.mdfor the dedicated write-side loader slice; this cutover pack tracks readiness, not all loader implementation details. - Completed first legacy IAM/Profile backfill validation scaffold:
migrations/legacy-to-go/002_migrate_auth.sqlnow targetshoctapaz_iam_db,migrations/legacy-to-go/003_migrate_users.sqltargetshoctapaz_profile_db, andscripts/migration/iam-profile-backfill-report.shruns both reports against the legacy database inside a read-only transaction. This report path remains read-only; write-side loading is handled only by the confirmed migrator flow. - Completed first IAM/Profile write-side loader preflight contract:
migrator validate --service iam-profile-backfillacceptsLEGACY_DATABASE_URL,IAM_DATABASE_URL, andPROFILE_DATABASE_URLor matching CLI flags, validates DSN separation plushoctapaz_iam_db/hoctapaz_profile_dbtarget ownership, emits JSON preflight evidence, and performs no database writes. Confirmed writes remain gated behindmigrator run --service iam-profile-backfill --confirm. - Completed first IAM/Profile write-side loader dry-run plan:
migrator dry-run --service iam-profile-backfillvalidates the same DSNs, emits deterministic JSON steps withownerDatabaseboundaries, target table groups,migration_runs/migration_id_mapcheckpoints, reconciliation evidence requirements, and a default-route cutover gate. This is still no-write planning evidence; target writes require the confirmed run. - Completed IAM/Profile write-side loader slices now load IAM identities, refresh tokens, derived sessions, provider settings/links, roles, permission catalog rows, role-permission mappings, user-role assignments, organization/member projections, parent-student access links, and teacher subject scopes plus explicit zero-source staff permission scope reconciliation, Profile users, contacts, education profiles, teacher KYC, and parent-child read models into owner databases with
migration_runs, row-levelmigration_id_map, and postload count/hash reconciliation. Production-like populated target validation, browser/default-promotion proof, final text-id cleanup, and default-route promotion remain blocked. - Added opt-in populated IAM/Profile backfill fixture smoke:
make test-iam-profile-backfill-populatedrunsscripts/test/iam-profile-backfill-populated-smoke.sh. It requiresIAM_PROFILE_BACKFILL_POPULATED_CONFIRM=populated-fixture, starts an isolated Postgres container, applies IAM/Profile target migrations, seeds representative legacy auth/profile rows, runs confirmedmigrator run --service iam-profile-backfill --confirm, checks reconciliation output including zero-sourcestaff_permission_scopes, and requiresiam_uuid_shadow_validationplusprofile_uuid_shadow_validationto report zero issues. It also asserts completedmigration_runsrows with source/target hash evidence. This proves loader/migration/UUID-shadow compatibility on a hermetic fixture only; production-like populated target validation, browser/default-promotion proof, and route promotion remain blocked. - Added read-only live populated target validation:
make test-iam-profile-live-populated-validationrunsscripts/test/iam-profile-live-populated-validation.sh. It requiresIAM_PROFILE_LIVE_VALIDATION_CONFIRM=live-populated,IAM_DATABASE_URL, andPROFILE_DATABASE_URL, validates the two owner DB names differ, wraps SQL in read-only transactions, requires non-empty IAM/Profile base rows, completedmigration_runsrows with source/target hash evidence,migration_id_maprows, and zero issues fromiam_uuid_shadow_validationplusprofile_uuid_shadow_validation. It also checks owner-local IAM/Profile orphan references. For cutover-grade evidence, setIAM_PROFILE_LIVE_VALIDATION_RUN_REPORTto the capturedmigrator run --service iam-profile-backfill --confirmJSON andIAM_PROFILE_LIVE_VALIDATION_REQUIRE_RUN_REPORT=1; the script then requires everypostload-reconciliationitem to be matched with zero issues, zero rejected rows, and source/target hashes. It passed on the populated fixture after the loader completedmigration_runs; production-like/live DSN proof remains a separate cutover gate. - Same-target fixture validation passed on 2026-07-09 under
output/qa/iam-profile-target-validation-20260709-114607/: the populated fixture was kept online after backfill, then live populated validation, UUID primary-key readiness, UUID FK validation, and UUID primary-key promotion validation all ran against the same IAM/Profile DSNs. The summaries recorded 22 matchedpostload-reconciliationitems, IAM/Profilemigration_id_maprows63/17, zero orphan references, readiness rows81/39with no blocking issues, validated FK constraints26/10, and promoted UUID PK metadata for21/9IAM/Profile tables with zero legacy text FKs. This remains fixture evidence, not production-like DSN approval. - Added static IAM/Profile route rollback rehearsal target:
make test-iam-profile-route-rollbackrunsscripts/test/iam-profile-route-rollback-rehearsal.sh. It validates the non-defaultauth-nativeandprofile-nativelocalhost route tables against the default rollback table, requires broadauth,admin, andfallbackroutes indeploy/gateway/routes.jsonto remainlegacy_proxy, and fails if exact IAM/Profile native carve-outs stay active in the default table. SetIAM_PROFILE_ROUTE_ROLLBACK_ARTIFACT_DIRto writesummary.jsonplus a Markdown rollback evidence note; optional live mode requiresIAM_PROFILE_ROUTE_ROLLBACK_CONFIRM=default-legacy. This is rollback evidence for the current route-table state, not default public promotion. - Local live rollback proof refreshed on 2026-07-09 with a standalone
api-gatewayonHTTP_ADDR=:18085,GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json, and dummyLEGACY_API_BASE_URL=http://127.0.0.1:9. The commandIAM_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-rollbackverified/v1/routesplus/api/auth/google/config,/api/auth/me/profile, and/api/admin/auth-settings/googleroute headers asauth/adminwithlegacy_proxy. The502response statuses were expected from the dummy legacy target; the proof is route-header rollback evidence only. Artifacts includesummary.json,live-routes.json, request headers/bodies, androllback-rehearsal.mdunderoutput/qa/iam-profile-route-rollback-20260709-114911/. - Completed first canonical IAM naming guard: service task rules now state that
/api/authand/v1/authare compatibility route namespaces only, while the deployable service, task pack, OpenAPI contract, deployment manifest, and runtime directory remainiam-service. The service-task coverage guard fails if anauth-serviceAPI/doc/deploy/runtime surface is reintroduced. - Add gateway route rehearsal, browser proof, and rollback notes before promoting additional
/api/auth/*routes. - Extend the completed IAM route guard into gateway cutover/rehearsal coverage and keep profile routes free of credential, JWT, role, or permission ownership.
- Completed first KYC ownership slice: profile-service owns teacher KYC storage, self submit/read, deterministic CCCD hash plus last4, redacted
TeacherKycSafe, andprofile.teacher_kyc.submittedaudit-shaped events. - Completed owner-side admin KYC review slice: profile-service now owns native
/v1/admin/users/kyc/teacher*list/detail/approve/reject over the same profile-owned storage, preserves pagination/search semantics, never returns plaintext CCCD orcccdNumberHash, and emits redactedprofile.teacher_kyc.reviewedevents. - Completed non-default gateway route-table rehearsal for teacher self-service KYC: exact
GET/POST /api/auth/me/kyc/teachercalls can route to profile-service/v1/users/{userId}/teacher-kycwith{userId}from the verified gateway JWT principal, a requiredTEACHERrole, no organization requirement, and broad auth/admin/fallback routes still legacy. - Completed non-default gateway route-table rehearsal for admin teacher KYC review: exact list plus detail/approve/reject suffix routes under
/api/admin/users/kyc/teacher*can route to profile-service/v1/admin/users/kyc/teacher*with a requiredADMINrole, no organization requirement, query/body preservation, and broad auth/admin/fallback routes still legacy. Admin-service wrapper decisions, default promotion, and browser/runtime evidence remain pending. - Completed first IAM management OpenAPI schema expansion: IAM auth/session and management routes now expose explicit request bodies and success-envelope response schemas for account directory, roles, permissions, account-role assignment, organization membership, access scopes, Google settings, and audit lists.
service-task-pack-coverage.shparses the IAM OpenAPI document and fails if these route schemas disappear before direct public promotion. - Completed first IAM Postgres parity expansion beyond invite-only coverage: the opt-in repository integration suite now covers role creation, permission assignment, account-role permission checks, organization member upsert/remove, identity-membership synchronization, and membership outbox payloads against an isolated migrated schema when
IAM_SERVICE_POSTGRES_TEST_DATABASE_URLis set. Clean Compose and local K8s live migration/runtime smoke proof now exists for IAM/Profile. - Prove default gateway cutover through route rehearsal, browser/runtime evidence, rollback notes, and user-service retirement criteria. The first default candidate boundary is now guarded by
make test-iam-profile-default-cutover-candidates: onlyauth-google-configand unauthenticatedauth-meare eligible for the next default-route review, while auth writes, profile writes, teacher KYC, and admin KYC routes remain blocked until route-specific evidence is attached.make test-iam-profile-default-candidate-diffgenerates the non-applying candidate JSON/diff bundle for that review and keeps the default route table legacy-safe. - Completed first profile update live gateway smoke:
PROFILE_ROUTE_LIVE_CONFIRM=profile-native PROFILE_ROUTE_LIVE_SEED_CONFIRM=seed-profilerunsmake test-profile-routes-liveagainst the non-default profile route table. Latest local artifacts underoutput/qa/profile-route-live-20260709-121055/proveprofile-base-update/native_write,profile-teacher-update/native_write, and broadauth/legacy_proxysibling routing. This does not satisfy populated-target validation, candidate default diff, rollback-after-candidate proof, or default-route promotion approval. - Completed first profile update browser smoke:
PROFILE_ROUTE_BROWSER_CONFIRM=profile-native PROFILE_ROUTE_BROWSER_SEED_CONFIRM=seed-profilerunsmake test-profile-routes-browseragainst the real/teacher/profilepage with the frontend pointed atapi-gateway. Latest local artifacts underoutput/playwright/profile-route-browser-smoke-20260709-121825/proveprofile-base-update/native_write,profile-teacher-update/native_write, and broadauth/legacy_proxysibling routing from a browser context. This is browser evidence for the non-default route table only; it does not promote default routes. - Keep runtime examples aligned with Compose service ports and service DNS. Example local env values must not point IAM school sync at unrelated services.
Acceptance:
- No deployable
auth-serviceor broad replacementuser-serviceis created. - IAM remains the only owner of credentials, sessions, JWT/JWKS, roles, permissions, and access-control membership.
- Profile remains the owner of display/profile and profile-verification/KYC data only; role/status/scopes stay projection metadata.
- UUID shadow migration, dual-write triggers, legacy backfill, UUID primary-key readiness views, owner-local UUID foreign-key validation, and UUID primary-key promotion are implemented with guard evidence. Default route promotion, production-like target validation, browser proof, and final text-id cleanup remain explicit cutover blockers.
- IAM/Profile backfill reports must stay read-only against the legacy database and must not reintroduce
hoctapaz_auth_db,hoctapaz_user_db, or any deployableauth-service/broaduser-serviceboundary. - UUID implementation follows
docs/microservices/iam-profile-uuid-cutover-design.md; PostgreSQL service-owned primary keys are UUID-backed in promoted target schemas while public OpenAPI ids remain string-compatible. - IAM management routes are protected by service-local authorization tests; gateway authorization proof now covers HS256 fallback and IAM RS256/JWKS verification before direct public exposure. IAM management routes also have explicit OpenAPI request/response schemas guarded by service-task coverage. IAM repository permission and organization-membership paths have memory coverage plus opt-in Postgres parity coverage.
- IAM password reset/change parity is implemented at native
/v1/auth/*service routes, with non-default gateway route rehearsal for the matching/api/auth/*paths. A read-only opt-in gateway smoke is available withAUTH_ROUTE_LIVE_CONFIRM=auth-native make test-auth-routes-live; it proves Google config and unauthenticated/api/auth/meroute through IAM while a sibling profile route remains legacy. That live smoke passed on 2026-07-09 against localiam-serviceandapi-gatewaywith artifacts underoutput/qa/auth-route-live-20260709-115133/: Google config returned200throughauth-google-config/native_read, unauthenticated/api/auth/mereturned401withAUTH_UNAUTHORIZEDthroughauth-me/native_read, and the sibling profile route stayed onauth/legacy_proxy. A matching opt-in browser proof is available withAUTH_ROUTE_BROWSER_CONFIRM=auth-native make test-auth-routes-browser; it exercises/auth/loginand the same IAM carve-outs from a page context. That browser proof passed on 2026-07-09 with artifacts underoutput/playwright/auth-route-browser-smoke-20260709-115940/and proved gateway-handled CORS preflight for the login pageX-Request-Idrequests.make test-iam-profile-route-rollbackvalidates rollback to the default legacy route table. Default-route promotion still remains blocked until live/browser proof is approved for the promoted route set and the remaining data/UUID blockers are closed. The default candidate browser wrapper is now available asmake test-iam-profile-default-candidate-browser; local proof underoutput/qa/iam-profile-default-candidate-browser-20260709-095159/uses the narrowcandidate-readonlyroute set and keeps login/register/admin/profile and KYC routes out of the candidate table. - IAM Google provider-link parity is implemented at native
/v1/auth/google*service routes, with non-default gateway route rehearsal for/api/auth/google*and admin settings; default-route/browser proof remains required before claiming legacy replacement. - Profile indexes, KYC ownership, and Profile OpenAPI request/response schemas are explicit enough for independent service agents to implement without guessing.
- Teacher KYC self-service responses keep plaintext CCCD, hashes, image URLs, permanent address, issuing metadata, verifier, and notes out of the safe profile/API surface; admin KYC review responses are admin-only, still omit plaintext CCCD/hash, and public route promotion remains separate cutover work.
- Non-default teacher self-service KYC route rehearsal remains exact, teacher-only, and rollback-safe; the default route table continues to keep
/api/auth/me/kyc/teacherlegacy until browser proof and rollback evidence are complete. - Non-default admin teacher KYC route rehearsal remains admin-only, suffix-guarded, and rollback-safe; the default route table continues to keep
/api/admin/users/kyc/teacher*legacy until browser proof and rollback evidence are complete. - Compose, static offline K8s, and Helm render IAM/Profile database bootstrap with persisted Postgres storage, init scripts, service-role DSNs,
IAM_JWKS_URLgateway/profile wiring, and migration-before-service ordering. - Default public route promotion has partial gateway/browser proof and rollback evidence for the current read-only auth candidates, plus non-default live/browser proof for the base and teacher profile update routes. Broader auth/profile/KYC promotion remains gated by production-like target validation, candidate default diffs, and route-specific rollback evidence.
- The final default route evidence bundle is recorded through
docs/agents/service-tasks/iam-profile-default-cutover-proof.md; do not mark this cutover complete from fixture-only loader proof or non-default route rehearsals.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/... ./services/profile-service/... ./services/api-gateway/internal/gateway -count=1IAM_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/internal/repository -run 'TestPostgres(InviteWorkflow|IAMDirectoryPermissionAndMembership)Integration' -count=1IAM_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/internal/repository -run TestPostgresIAMUUIDShadowDualWriteIntegration -count=1PROFILE_SERVICE_POSTGRES_TEST_DATABASE_URL=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/profile-service/internal/repository -run TestPostgresProfileUUIDShadowDualWriteIntegration -count=1make test-auth-routes test-profile-routes test-service-task-packsmake 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-browserPROFILE_ROUTE_LIVE_SELF_TEST=1 make test-profile-routes-livePROFILE_ROUTE_BROWSER_SELF_TEST=1 make test-profile-routes-browsermake test-iam-profile-backfill-reportmake test-iam-profile-backfill-loaderIAM_PROFILE_BACKFILL_POPULATED_SELF_TEST=1 make test-iam-profile-backfill-populatedIAM_PROFILE_LIVE_VALIDATION_SELF_TEST=1 make test-iam-profile-live-populated-validationIAM_PROFILE_UUID_PK_READINESS_SELF_TEST=1 make test-iam-profile-uuid-primary-key-readinessIAM_PROFILE_UUID_FK_VALIDATION_SELF_TEST=1 make test-iam-profile-uuid-foreign-key-validationIAM_PROFILE_UUID_PK_PROMOTION_SELF_TEST=1 make test-iam-profile-uuid-primary-key-promotion- Optional populated fixture smoke:
IAM_PROFILE_BACKFILL_POPULATED_CONFIRM=populated-fixture make test-iam-profile-backfill-populated - Optional read-only populated target smoke; not sufficient for cutover:
IAM_PROFILE_LIVE_VALIDATION_CONFIRM=live-populated IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable make test-iam-profile-live-populated-validation - Cutover gate for populated target validation:
IAM_PROFILE_LIVE_VALIDATION_CONFIRM=live-populated IAM_PROFILE_LIVE_VALIDATION_RUN_REPORT=/path/to/migrator-run.json IAM_PROFILE_LIVE_VALIDATION_REQUIRE_RUN_REPORT=1 IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable make test-iam-profile-live-populated-validation - Optional UUID primary-key readiness validation:
IAM_PROFILE_UUID_PK_READINESS_CONFIRM=uuid-primary-key-readiness IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable make test-iam-profile-uuid-primary-key-readiness - Optional UUID foreign-key validation:
IAM_PROFILE_UUID_FK_VALIDATION_CONFIRM=uuid-foreign-key-validation IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable make test-iam-profile-uuid-foreign-key-validation - Optional UUID primary-key promotion validation:
IAM_PROFILE_UUID_PK_PROMOTION_CONFIRM=uuid-primary-key-promotion IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable make test-iam-profile-uuid-primary-key-promotion sh scripts/test/service-task-pack-coverage.shmake test-iam-profile-runtime-proofIAM_PROFILE_CLEAN_COMPOSE_SELF_TEST=1 make test-iam-profile-clean-composeIAM_PROFILE_K8S_SELF_TEST=1 make test-iam-profile-local-k8s- Optional live clean Compose smoke:
IAM_PROFILE_CLEAN_COMPOSE_CONFIRM=clean-runtime make test-iam-profile-clean-compose - Optional local-image live smoke when service images are already built:
IAM_PROFILE_CLEAN_COMPOSE_CONFIRM=clean-runtime IAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1 IAM_PROFILE_REBUILD_MIGRATOR_IMAGE=1 make test-iam-profile-clean-compose - Optional local/offline K8s smoke after images are built and loaded:
IAM_PROFILE_K8S_LIVE_CONFIRM=local-k8s make test-iam-profile-local-k8s - OpenAPI YAML parse and mirror check for IAM/Profile service and contract files.
make test-runtime-foundationpnpm docs:build