Appearance
IAM/Profile Default Cutover Proof
Status: Pending.
This document is the evidence template for promoting selected default /api/auth*, profile, or KYC compatibility routes from legacy proxying to native iam-service and profile-service through api-gateway.
Do not use this page as approval until every section below is filled with real runtime artifacts from the target environment. Fixture-only loader proof, non-default route rehearsal, or direct service HTTP checks are not enough.
Candidate Routes
Candidate list defined on 2026-07-09. This is not approval to promote deploy/gateway/routes.json; it is the conservative candidate boundary for the next default-route review. deploy/gateway/routes.json still keeps broad auth, broad admin, and fallback as legacy_proxy.
Eligible for next default candidate review:
| Route | Method | Public path | Native owner | Evidence | Remaining gates |
|---|---|---|---|---|---|
auth-google-config | GET | /api/auth/google/config | iam-service | Refreshed live gateway: output/qa/auth-route-live-20260710-1543/; candidate browser: output/playwright/iam-profile-default-candidate-browser-20260710-1548/; prior browser baseline: output/playwright/auth-route-browser-smoke-20260709-115940/ | Attach production-like populated validation with required run report and rollback proof after preparing the promotion diff |
auth-me | GET | /api/auth/me | iam-service | Refreshed live gateway: output/qa/auth-route-live-20260710-1543/; candidate browser: output/playwright/iam-profile-default-candidate-browser-20260710-1548/; prior browser baseline: output/playwright/auth-route-browser-smoke-20260709-115940/ | Attach production-like populated validation with required run report, session restore browser proof for authenticated users, and rollback proof after preparing the promotion diff |
Historical local gateway evidence remains under output/qa/auth-route-live-20260709-115133/; the current candidate chain does not broaden the approved route set.
Blocked native rehearsals:
| Route | Reason default promotion remains blocked |
|---|---|
auth-register | Write route lacks populated-target/browser rollback evidence. |
auth-login | Browser session lifecycle proof exists in output/playwright/auth-route-session-browser-smoke-20260709-100744/; populated-target validation, candidate default diff, and rollback-after-candidate evidence are still missing. |
auth-refresh | Browser refresh rotation, old-refresh rejection, session restore, and refresh-after-logout proof exist in output/playwright/auth-route-session-browser-smoke-20260709-100744/; populated-target validation, candidate default diff, and rollback-after-candidate evidence are still missing. |
auth-logout | Browser logout proof exists in output/playwright/auth-route-session-browser-smoke-20260709-100744/; populated-target validation, candidate default diff, and rollback-after-candidate evidence are still missing. |
auth-me-password | Password change lacks browser proof and rollback evidence. |
auth-forgot-password | Password reset request lacks browser proof and token-safety review. |
auth-reset-password | Password reset completion lacks browser proof and token-safety review. |
auth-google | Google callback/linking write route lacks provider-token browser proof. |
admin-auth-google-settings-read | Admin Google settings lacks admin browser proof. |
admin-auth-google-settings-write | Admin Google settings write lacks admin browser proof. |
profile-base-update | Live gateway proof exists in output/qa/profile-route-live-20260709-121055/ and browser proof exists in output/playwright/profile-route-browser-smoke-20260709-121825/; populated-target validation, candidate default diff, and rollback-after-candidate evidence are still missing. |
profile-teacher-update | Live gateway proof exists in output/qa/profile-route-live-20260709-121055/ and browser proof exists in output/playwright/profile-route-browser-smoke-20260709-121825/; populated-target validation, candidate default diff, and rollback-after-candidate evidence are still missing. |
profile-student-update | Student profile write route lacks live/browser update proof. |
profile-parent-update | Parent profile write route lacks live/browser update proof. |
profile-kyc-read | Teacher KYC self-service lacks browser/runtime proof. |
profile-kyc-submit | Teacher KYC submit lacks browser/runtime proof. |
profile-admin-kyc-list | Admin KYC list lacks admin browser/runtime proof. |
profile-admin-kyc-detail | Admin KYC detail lacks admin browser/runtime proof. |
profile-admin-kyc-approve | Admin KYC approve lacks admin browser/runtime proof. |
profile-admin-kyc-reject | Admin KYC reject lacks admin browser/runtime proof. |
Guard:
bash
make test-iam-profile-default-cutover-candidatesDisposable Populated Candidate Regression
Run this opt-in proof to reproduce the candidate chain without a shared or legacy database:
bash
IAM_PROFILE_DEFAULT_CANDIDATE_ISOLATED_CONFIRM=disposable-populated \
make test-iam-profile-default-candidate-isolatedIt starts a fresh Postgres container, backfills the representative IAM/Profile fixture into the two owner databases, validates the migration/UUID evidence on those exact databases, starts IAM, Profile, API Gateway, and public-web, then captures the read-only auth-google-config and auth-me candidate through a real browser. It restarts the isolated Gateway with deploy/gateway/routes.json for the rollback header check and removes all processes and the container by default.
The resulting summary.json is explicitly targetProvenance=disposable-populated, routePromotionApproved=false, and defaultRouteTableModified=false. It strengthens regression coverage only; it does not satisfy the production-like populated target, promotion review, or default-route requirements below.
Latest disposable run: output/qa/iam-profile-default-candidate-isolated-20260714-014803-522/ passed with four backfilled accounts, owner-local UUID validation, actual candidate Gateway/browser headers for auth-google-config and auth-me, and the separate rollback Gateway preserving broad auth, admin, and fallback as legacy_proxy. The proof removed its temporary processes and Postgres container after capture.
The guard fails when an IAM/Profile native rehearsal route is missing from the eligible/blocked list, when the current default route table activates a pending candidate, or when the linked live/browser evidence paths disappear from the auth/profile smoke docs.
Proof Bundle
For a production-like populated target review, run the bundle wrapper so the populated target validation, UUID checks, candidate diff, gateway live proof, browser proof, and rollback proof all land under one artifact root:
bash
IAM_PROFILE_DEFAULT_CUTOVER_PROOF_BUNDLE_CONFIRM=default-cutover-proof \
IAM_PROFILE_DEFAULT_CUTOVER_TARGET_PROVENANCE=production-like \
IAM_PROFILE_LIVE_VALIDATION_MIN_ACCOUNTS=<expected-min-account-rows> \
IAM_PROFILE_LIVE_VALIDATION_MIN_MAP_ROWS=<expected-min-migration-map-rows> \
IAM_PROFILE_LIVE_VALIDATION_RUN_REPORT=/path/to/migrator-run.json \
IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable \
PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable \
GATEWAY_BASE_URL=http://localhost:<gateway-port> \
WEB_BASE_URL=http://localhost:<web-port> \
make test-iam-profile-default-cutover-proof-bundleThe wrapper writes:
summary.jsonlive-validation/uuid-readiness/uuid-fk-validation/uuid-pk-promotion/candidate-diff/candidate-live/candidate-browser/candidate-rollback/
The summary must record routePromotionApproved=false, defaultRouteTableModified=false, mode=live, targetProvenance=production-like, all steps passed, candidateRouteTableHash, gatewayRouteTableHash, and the generated candidate/rollback artifact paths. The candidate diff, live, browser, and rollback summaries must all carry the same candidateRouteTableHash; the live summary must also prove the gateway /v1/routes hash matches that candidate hash. The captured command must not set any *_SELF_TEST=1 variables. A passed proof bundle is still evidence for human review; it does not edit or approve deploy/gateway/routes.json.
Offline Artifact Review
After a production-like proof bundle has been captured, validate the artifact tree itself before asking for default-route promotion review:
bash
IAM_PROFILE_DEFAULT_CUTOVER_PROOF_BUNDLE_ARTIFACT_DIR=output/qa/iam-profile-default-cutover-proof-bundle-<timestamp> \
make test-iam-profile-default-cutover-review-artifactThe review validator is offline and read-only. It does not connect to PostgreSQL, does not call api-gateway or browser targets, and does not edit deploy/gateway/routes.json. It rejects fixture/self-test bundles and requires the supplied artifact to contain:
- Root
summary.jsonwithstatus=passed,mode=live,selfTestOnly=false,targetProvenance=production-like,routePromotionApproved=false, anddefaultRouteTableModified=false. requiredLiveMinimums.accountsandrequiredLiveMinimums.migrationIdMapRowsboth greater than1.- Matching
candidateRouteTableHashandgatewayRouteTableHashwith norouteTableHashFailures. - Step summaries for populated validation, UUID readiness/FK/PK promotion, candidate diff, candidate live, candidate browser, and candidate rollback.
- Populated validation summary with
readOnly=true,runReport.status=passed,runReport.required=true, non-emptypostloadReconciliationItems, and bothiam-serviceandprofile-servicereconciliation services. - Candidate diff/live/browser/rollback summaries whose candidate route-table hash matches the bundle root and whose
defaultRouteTableModified=false.
The script also has a hermetic guard for the validator itself:
bash
IAM_PROFILE_DEFAULT_CUTOVER_REVIEW_ARTIFACT_SELF_TEST=1 \
bash scripts/test/iam-profile-default-cutover-review-artifact.shLatest Artifact Triage
Latest local bundle checked on 2026-07-15:
text
output/qa/iam-profile-default-cutover-proof-bundle-20260715-053933/This artifact is not cutover-grade. All eight child self-tests passed, and the root summary records status=passed, but it also records mode=self-test, selfTestOnly=true, targetProvenance=fixture_only, no required live minimums, and no candidateRouteTableHash / gatewayRouteTableHash.
The wrapper now writes a summary.json under every self-test step directory, which makes the artifact tree inspectable without changing its evidence class.
The offline review guard rejected it as expected:
bash
IAM_PROFILE_DEFAULT_CUTOVER_PROOF_BUNDLE_ARTIFACT_DIR=output/qa/iam-profile-default-cutover-proof-bundle-20260715-053933 \
make test-iam-profile-default-cutover-review-artifactReject reasons included: bundle mode must be live, selfTestOnly must be false, target provenance must be production-like, live minimum account and migration_id_map row counts must be greater than one, route-table hashes must be present and matching, and each step must include its own summary.json.
The validator's hermetic self-test also passed on 2026-07-15 with IAM_PROFILE_DEFAULT_CUTOVER_REVIEW_ARTIFACT_SELF_TEST=1 make test-iam-profile-default-cutover-review-artifact. It validates the reviewer, not the self-test bundle's eligibility for promotion.
The configured legacy source was also checked through the read-only report pipeline on 2026-07-10. The report did not connect because the configured PostgreSQL endpoint refused the connection; no mutation was attempted. There is therefore still no source-side production-like report to attach to the target validation bundle.
Populated Target Validation
Required command:
bash
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_PROFILE_LIVE_VALIDATION_MIN_ACCOUNTS=<expected-min-account-rows> \
IAM_PROFILE_LIVE_VALIDATION_MIN_MAP_ROWS=<expected-min-migration-map-rows> \
IAM_DATABASE_URL=postgres://.../hoctapaz_iam_db?sslmode=disable \
PROFILE_DATABASE_URL=postgres://.../hoctapaz_profile_db?sslmode=disable \
make test-iam-profile-live-populated-validationEvidence requirements:
- Target DSNs are redacted but identify separate IAM/Profile owner databases.
postload-reconciliationitems are all matched.- Zero issues and zero rejected rows are recorded.
- Source and target hashes are present.
- The validation summary records the migrator report top-level status/message; this source report status is loader-scope context and cannot replace the required
runReport.status=passedreconciliation result. migration_runsandmigration_id_mapevidence exists in both owner databases.- IAM/Profile owner-local orphan checks are clean.
Latest same-target fixture rehearsal on 2026-07-10:
text
output/qa/iam-profile-target-validation-20260710/It passed the read-only populated validation and UUID readiness/FK/PK promotion checks with four seeded accounts, IAM/Profile migration map rows, zero UUID/orphan issues, and 22 matched postload reconciliation items. This remains fixture-only evidence and cannot satisfy the required targetProvenance=production-like cutover gate.
Candidate Default Diff
Generate the review-only candidate route table and unified diff without editing deploy/gateway/routes.json:
bash
make test-iam-profile-default-candidate-diffLatest generated artifact on 2026-07-10:
text
output/qa/iam-profile-default-candidate-diff-20260710-071914/The candidate diff is allowed to add only auth-google-config and auth-me from deploy/gateway/routes.auth-native-example.json before the broad auth/legacy_proxy route. The summary must keep defaultRouteTableModified=false, preserve broad auth, admin, and fallback as legacy_proxy, list every blocked auth/profile/KYC route as not activated, and record candidateRouteTableHash.
This artifact generated:
candidate-routes.jsoncandidate-routes.diffsummary.json
The summary recorded status=review_only, defaultRouteTableModified=false, eligible routes auth-google-config and auth-me, all blocked auth/profile/KYC routes as not activated, and broad auth, admin, plus fallback preserved as legacy_proxy.
UUID Metadata Validation
Run the UUID readiness, FK validation, and primary-key promotion checks against the same populated target DSNs:
bash
make test-iam-profile-uuid-primary-key-readiness
make test-iam-profile-uuid-foreign-key-validation
make test-iam-profile-uuid-primary-key-promotionRecord command lines with their confirmation variables, redacted DSNs, and result artifacts.
Gateway Runtime Proof
Record api-gateway live smoke evidence for the promoted route set:
- Request URL and method.
- Response status.
- Gateway route headers.
- Request id.
- Legacy sibling route checks proving non-candidate routes still follow the intended legacy route.
For local review without editing deploy/gateway/routes.json, generate a candidate table that targets the local iam-service port and start api-gateway with that generated table:
bash
IAM_PROFILE_AUTH_NATIVE_ROUTE_TABLE=deploy/gateway/routes.auth-native-localhost-example.json \
IAM_PROFILE_DEFAULT_CANDIDATE_TARGET_OVERRIDE=http://localhost:18092 \
IAM_PROFILE_DEFAULT_CANDIDATE_DIFF_DIR=output/qa/iam-profile-default-candidate-localhost-<timestamp> \
make test-iam-profile-default-candidate-diffThen run:
bash
IAM_PROFILE_DEFAULT_CANDIDATE_LIVE_CONFIRM=auth-candidate \
IAM_PROFILE_DEFAULT_CANDIDATE_ROUTE_TABLE=output/qa/iam-profile-default-candidate-localhost-<timestamp>/candidate-routes.json \
IAM_PROFILE_DEFAULT_CANDIDATE_DIFF_SUMMARY=output/qa/iam-profile-default-candidate-localhost-<timestamp>/summary.json \
GATEWAY_BASE_URL=http://localhost:18093 \
make test-iam-profile-default-candidate-liveThe smoke writes auth-route-live headers/bodies plus a candidate summary and must show defaultRouteTableModified=false, route headers for auth-google-config/native_read and auth-me/native_read, all blocked auth/profile/KYC routes inactive, broad auth, admin, plus fallback preserved as legacy_proxy, and gatewayRouteTableHash equal to candidateRouteTableHash.
sha256(normalized ordered routes) means the route list after the same prefix, exact, and suffix-specific ordering applied by api-gateway. The contract therefore verifies the table Gateway actually serves at /v1/routes, rather than relying on the original JSON file order.
Latest local candidate gateway proof on 2026-07-10:
text
output/qa/iam-profile-default-candidate-localhost-20260710-1544/
output/qa/iam-profile-default-candidate-live-20260710-1547/Summary:
| Check | Evidence |
|---|---|
| Candidate table | candidate-routes.json generated from deploy/gateway/routes.json plus routes.auth-native-localhost-example.json with target override http://127.0.0.1:8081 |
| Default table | defaultRouteTableModified=false |
| Google config | GET /api/auth/google/config returned 200, route auth-google-config, state native_read |
| Auth me | unauthenticated GET /api/auth/me returned 401, route auth-me, state native_read |
| Legacy sibling | GET /api/auth/me/profile?source=auth-live-smoke returned expected dummy-legacy 502, route auth, state legacy_proxy |
| Blocked routes | all blocked auth/profile/KYC routes listed in blockedRoutesNotActivated |
Current Isolated Candidate Recheck (2026-07-13)
output/qa/iam-profile-default-candidate-isolated-20260713-121616/ captures a fresh, separately named Compose project. It built the current IAM, Profile, and Gateway images, mounted only the generated candidate table into a standalone Gateway on 127.0.0.1:18093, and left deploy/gateway/routes.json unchanged. IAM and Profile used fresh project-owned volumes; the shared Gateway and legacy source were not used.
The generated candidate added only auth-google-config and auth-me before broad auth/legacy_proxy. Its candidate and served Gateway route-table hashes both equal sha256:1c6fbb3fd5c2ae2426d7f8eb5917a5841bfe9864de86f1a45cba653cc86a6964. The live smoke recorded 200 auth-google-config/native_read, 401 auth-me/native_read, and 404 auth/legacy_proxy for the sibling route. The public-web browser smoke repeated those three route/header checks from /auth/login. After replacing only the standalone Gateway container with the unchanged default table, the rollback smoke observed auth/legacy_proxy for Google config and the profile sibling plus admin/legacy_proxy for Google settings. All three artifact summaries record defaultRouteTableModified=false.
This is stronger local runtime evidence than a self-test, but it is still an isolated, non-default candidate recheck. The project used fresh data, has no production-like migrator run report, and does not satisfy the populated-target or public/default promotion gates.
Candidate Browser Proof
For local review without editing deploy/gateway/routes.json, run the browser proof against the same generated candidate table used by the gateway runtime proof:
bash
IAM_PROFILE_DEFAULT_CANDIDATE_BROWSER_CONFIRM=auth-candidate \
IAM_PROFILE_DEFAULT_CANDIDATE_ROUTE_TABLE=output/qa/iam-profile-default-candidate-localhost-<timestamp>/candidate-routes.json \
IAM_PROFILE_DEFAULT_CANDIDATE_DIFF_SUMMARY=output/qa/iam-profile-default-candidate-localhost-<timestamp>/summary.json \
WEB_BASE_URL=http://localhost:<web-port> \
GATEWAY_BASE_URL=http://localhost:<gateway-port> \
make test-iam-profile-default-candidate-browserThe wrapper runs the auth route Playwright smoke with AUTH_ROUTE_BROWSER_EXPECT_ROUTE_SET=candidate-readonly. The candidate route table may contain only auth-google-config and auth-me as native read routes; broad auth, broad admin, and fallback must stay legacy_proxy, blocked auth/profile/KYC routes must not be active, and the browser proof summary must repeat the same candidateRouteTableHash from the generated candidate diff.
Latest local candidate browser proof on 2026-07-10:
text
output/playwright/iam-profile-default-candidate-browser-20260710-1548/Summary:
| Check | Evidence |
|---|---|
| Browser route set | AUTH_ROUTE_BROWSER_EXPECT_ROUTE_SET=candidate-readonly |
| Web app | public-web on http://127.0.0.1:5205, with VITE_API_BASE_URL=http://127.0.0.1:18093 |
| Candidate table | output/qa/iam-profile-default-candidate-localhost-20260710-1544/candidate-routes.json |
| Google config | browser-originated GET /api/auth/google/config returned 200, route auth-google-config, state native_read |
| Auth me | browser-originated unauthenticated GET /api/auth/me returned 401, route auth-me, state native_read |
| Legacy sibling | browser-originated GET /api/auth/me/profile?source=auth-browser-smoke returned expected dummy-legacy 502, route auth, state legacy_proxy |
| Default table | defaultRouteTableModified=false |
Candidate Rollback Proof
For local review without editing deploy/gateway/routes.json, run the rollback proof after preparing the same generated candidate table:
bash
IAM_PROFILE_DEFAULT_CANDIDATE_ROLLBACK_CONFIRM=auth-candidate-rollback \
IAM_PROFILE_DEFAULT_CANDIDATE_ROUTE_TABLE=output/qa/iam-profile-default-candidate-localhost-<timestamp>/candidate-routes.json \
IAM_PROFILE_DEFAULT_CANDIDATE_DIFF_SUMMARY=output/qa/iam-profile-default-candidate-localhost-<timestamp>/summary.json \
GATEWAY_BASE_URL=http://localhost:<gateway-port> \
make test-iam-profile-default-candidate-rollbackThe wrapper validates that the candidate table activates only auth-google-config and auth-me, then runs iam-profile-route-rollback-rehearsal.sh against a gateway using deploy/gateway/routes.json. The rollback table must route /api/auth/google/config, /api/auth/me/profile, and /api/admin/auth-settings/google through broad auth or adminlegacy_proxy routes, with defaultRouteTableModified=false.
Latest local candidate rollback proof on 2026-07-10:
text
output/qa/iam-profile-default-candidate-rollback-20260710-1548/Summary:
| Check | Evidence |
|---|---|
| Candidate table | output/qa/iam-profile-default-candidate-localhost-20260710-1544/candidate-routes.json |
| Gateway rollback table | deploy/gateway/routes.json served by api-gateway on http://localhost:18094 |
| Google config rollback | GET /api/auth/google/config returned expected dummy-legacy 502, route auth, state legacy_proxy |
| Profile sibling rollback | GET /api/auth/me/profile?source=iam-profile-rollback-rehearsal returned expected dummy-legacy 502, route auth, state legacy_proxy |
| Admin Google settings rollback | GET /api/admin/auth-settings/google returned expected dummy-legacy 502, route admin, state legacy_proxy |
| Broad rollback routes | auth, admin, and fallback recorded as legacy_proxy |
| Default table | defaultRouteTableModified=false |
Browser Proof
Record browser evidence through frontend pages that call api-gateway or an approved BFF only. Do not include raw tokens, passwords, reset tokens, Google tokens, or provider secrets in screenshots, console logs, or telemetry.
Minimum evidence by promoted route group:
- Login/session restore/logout.
- Password reset/change if those routes are promoted.
- Google auth entry/config if those routes are promoted.
- Profile updates if profile routes are promoted.
- Teacher/admin KYC if KYC routes are promoted.
Current profile-update browser evidence is recorded under output/playwright/profile-route-browser-smoke-20260709-121825/, proving profile-base-update/native_write, profile-teacher-update/native_write, and the auth/legacy_proxy sibling from the real /teacher/profile page. It is supporting evidence only; profile routes remain blocked from default promotion until populated target validation, a candidate default diff, and rollback proof after that diff are attached.
Current auth session lifecycle browser evidence is recorded under output/playwright/auth-route-session-browser-smoke-20260709-100744/, proving auth-register/native_write, auth-login/native_write, auth-refresh/native_write, auth-logout/native_write, authenticated auth-me/native_read, refresh-token rotation, old-refresh rejection, and refresh-after-logout rejection from a browser page context. It is supporting evidence only; those write routes remain blocked from default promotion until populated target validation, candidate default diffs, and rollback-after-candidate evidence are attached.
Rollback Proof
Run rollback rehearsal after the promotion candidate is prepared:
bash
IAM_PROFILE_ROUTE_ROLLBACK_CONFIRM=default-legacy make test-iam-profile-route-rollbackRecord route-header checks for:
- One IAM route.
- One profile or KYC route when profile/KYC routes are promoted.
- One broad legacy sibling route.
- Fallback routing.
Decision
Leave this section as Pending until all validation, gateway, browser, and rollback evidence is attached. Final text-id cleanup is a separate migration task after default route behavior and rollback are proven.