Skip to content

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 AuthService is 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-service
  • contracts/openapi/services/iam-service.yaml
  • docs/api/iam-service.md
  • services/iam-service/docs/ARCHITECTURE.md
  • services/iam-service/docs/API.md
  • services/iam-service/docs/DATABASE.md

Profile service:

  • services/profile-service
  • contracts/openapi/services/profile-service.yaml
  • docs/api/profile-service.md
  • services/profile-service/docs/ARCHITECTURE.md
  • services/profile-service/docs/API.md
  • services/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.sql
  • services/iam-service/migrations/000002_auth_identity.sql
  • services/iam-service/migrations/000003_iam_directory.sql
  • services/iam-service/migrations/000004_iam_invites_revoke.sql
  • services/iam-service/migrations/000005_iam_membership_outbox.sql
  • services/iam-service/migrations/000006_iam_password_reset.sql
  • services/iam-service/migrations/000007_iam_google_auth.sql
  • services/iam-service/migrations/000008_iam_migration_runs.sql
  • services/iam-service/migrations/000009_iam_uuid_shadow_columns.sql
  • services/iam-service/migrations/000010_iam_uuid_shadow_dual_write.sql
  • services/iam-service/migrations/000011_iam_uuid_primary_key_readiness.sql
  • services/iam-service/migrations/000012_iam_uuid_foreign_key_constraints.sql
  • services/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.sql
  • services/profile-service/migrations/000002_user_profiles.sql
  • services/profile-service/migrations/000003_profile_service_boundary.sql
  • services/profile-service/migrations/000004_profile_query_indexes.sql
  • services/profile-service/migrations/000005_profile_migration_runs.sql
  • services/profile-service/migrations/000006_profile_uuid_shadow_columns.sql
  • services/profile-service/migrations/000007_profile_uuid_shadow_dual_write.sql
  • services/profile-service/migrations/000008_profile_uuid_primary_key_readiness.sql
  • services/profile-service/migrations/000009_profile_uuid_foreign_key_constraints.sql
  • services/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/register
  • POST /v1/auth/login
  • POST /v1/auth/logout
  • POST /v1/auth/refresh
  • POST /v1/auth/forgot-password
  • POST /v1/auth/reset-password
  • GET /v1/auth/google/config
  • POST /v1/auth/google
  • GET /v1/auth/me
  • POST /v1/auth/me/password
  • GET /v1/.well-known/jwks.json
  • GET /.well-known/jwks.json
  • POST /v1/accounts
  • GET /v1/accounts/{accountId}
  • PATCH /v1/accounts/{accountId}
  • DELETE /v1/accounts/{accountId}
  • GET /v1/roles
  • POST /v1/roles
  • PATCH /v1/roles/{roleId}
  • DELETE /v1/roles/{roleId}
  • GET /v1/permissions
  • POST /v1/permissions
  • POST /v1/roles/{roleId}/permissions
  • POST /v1/accounts/{accountId}/roles
  • DELETE /v1/accounts/{accountId}/roles/{roleId}
  • GET /v1/accounts/{accountId}/permissions/{permissionCode}/check
  • GET /v1/organizations
  • POST /v1/organizations
  • GET /v1/organizations/{organizationId}/members
  • POST /v1/organizations/{organizationId}/members
  • DELETE /v1/organizations/{organizationId}/members/{accountId}
  • GET /v1/accounts/{accountId}/parent-student-links
  • POST /v1/accounts/{accountId}/parent-student-links
  • DELETE /v1/accounts/{accountId}/parent-student-links/{linkId}
  • GET /v1/accounts/{accountId}/teacher-subject-scopes
  • POST /v1/accounts/{accountId}/teacher-subject-scopes
  • DELETE /v1/accounts/{accountId}/teacher-subject-scopes/{scopeId}
  • GET /v1/accounts/{accountId}/staff-permission-scopes
  • POST /v1/accounts/{accountId}/staff-permission-scopes
  • DELETE /v1/accounts/{accountId}/staff-permission-scopes/{scopeId}
  • POST /v1/invites
  • POST /v1/invites/accept
  • POST /v1/invites/{inviteId}/revoke
  • GET /v1/admin/auth-settings/google
  • PATCH /v1/admin/auth-settings/google
  • GET /v1/audit/login
  • GET /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/profiles
  • GET /v1/profiles/{accountId}
  • PATCH /v1/profiles/{accountId}
  • GET /v1/profiles/{accountId}/public
  • GET /v1/profiles/{accountId}/contacts
  • POST /v1/profiles/{accountId}/contacts
  • DELETE /v1/profiles/{accountId}/contacts/{contactId}
  • GET /v1/profiles/{accountId}/children
  • PUT /v1/internal/profiles/{accountId}/children/{studentAccountId}
  • DELETE /v1/internal/profiles/{accountId}/children/{studentAccountId}
  • POST /v1/profiles/{accountId}/student
  • GET /v1/profiles/{accountId}/student
  • PATCH /v1/profiles/{accountId}/student
  • POST /v1/profiles/{accountId}/teacher
  • GET /v1/profiles/{accountId}/teacher
  • PATCH /v1/profiles/{accountId}/teacher
  • POST /v1/profiles/{accountId}/parent
  • GET /v1/profiles/{accountId}/parent
  • PATCH /v1/profiles/{accountId}/parent
  • POST /v1/profiles/{accountId}/staff
  • GET /v1/profiles/{accountId}/staff
  • PATCH /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_MINUTES controls IAM access-token lifetime.
  • JWT_REFRESH_TTL_DAYS controls IAM refresh-token lifetime.
  • PASSWORD_HASH_COST controls IAM bcrypt cost.
  • IAM_JWKS_URL points profile/gateway integrations at the IAM JWKS endpoint. Gateway uses it for RS256 route auth and falls back to AUTH_JWT_SECRET for HS256 local rehearsal tokens.

8. Local Development

Local Compose wiring exists in deploy/docker-compose.yml:

  • iam-service on host port 8081
  • profile-service on host port 8082
  • separate service-owned PostgreSQL databases in the shared local Postgres container
  • service-owned IAM/Profile database roles for runtime and migration jobs
  • postgres-local-data persistence 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.example
  • services/profile-service/.env.example

Service Makefiles support:

  • make run
  • make test
  • make migrate-up
  • make migrate-down
  • make docker-build
  • make lint

Gateway rehearsal route tables:

  • deploy/gateway/routes.auth-native-example.json
  • deploy/gateway/routes.auth-native-localhost-example.json
  • deploy/gateway/routes.profile-native-example.json
  • deploy/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.yaml
  • deploy/k8s/services/profile-service.yaml
  • deploy/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-routes
  • make test-iam-profile-route-rollback
  • IAM_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-rollback
  • AUTH_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-live
  • AUTH_ROUTE_LIVE_SELF_TEST=1 make test-auth-routes-live
  • AUTH_ROUTE_BROWSER_SELF_TEST=1 make test-auth-routes-browser
  • AUTH_SESSION_BROWSER_SELF_TEST=1 make test-auth-routes-session-browser
  • AUTH_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_URL is set.
  • sh scripts/test/service-task-pack-coverage.sh parses 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-proof verifies 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, profile IAM_JWKS_URL, local images, and migration jobs.
  • make test-iam-profile-backfill-loader verifies the no-write migrator validate --service iam-profile-backfill preflight: legacy, IAM, and Profile DSNs stay separate; IAM targets hoctapaz_iam_db; Profile targets hoctapaz_profile_db; invalid target DB names fail before any DB connection or write. It also verifies migrator dry-run --service iam-profile-backfill emits 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_runs schema plus migration_id_map.migration_run_id evidence columns before any write-side loader is accepted.
  • Confirmed migrator run --service iam-profile-backfill --confirm now reads legacy User, RefreshToken, AuthProviderSetting, UserAuthProvider, Organization, OrganizationMember, TeacherProfile, StudentProfile, ParentProfile, TeacherKyc, and ParentStudent rows inside pgx read-only transactions. The loader upserts base rows into IAM identities, IAM refresh_tokens, IAM derived sessions, IAM auth_provider_settings, IAM user_auth_providers, IAM roles, IAM permissions, IAM role_permissions, IAM user_roles, IAM organizations, IAM organization_members, IAM identity_memberships, IAM parent_student_links, IAM teacher_subject_scopes, and Profile users. It also emits an explicit zero-source IAM staff_permission_scopes reconciliation because legacy has no per-account staff permission scope table, then loads Profile profile_contacts derived from legacy email/phone fields, Profile teacher_profiles, student_profiles, parent_profiles, teacher_kyc, and parent_students, writes owner-local migration_id_map rows linked to migration_runs, and reports rowsWritten.
  • The same run now emits postload reconciliation items for each loaded IAM/Profile owner table with sourceRows, targetRows, sourceHash, targetHash, matched, issueCount, and rejectedRows; it also updates each owner database migration_runs row with source and target digest hashes and marks the owner run completed when 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-populated validates the populated-smoke script parser/fixture guard. The live fixture command is IAM_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 confirmed migrator run --service iam-profile-backfill --confirm, asserts reconciliation rows including zero-source staff_permission_scopes, and checks iam_uuid_shadow_validation plus profile_uuid_shadow_validation. The fixture now also asserts completed migration_runs rows 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-populated passed. The captured backfill-run.json recorded 4 legacy user rows, IAM rowsWritten=63, Profile rowsWritten=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-validation validates the read-only live populated validation script. Against the same populated fixture, IAM_PROFILE_LIVE_VALIDATION_CONFIRM=live-populated verified non-empty IAM/Profile base rows, completed migration_runs with hash evidence, populated migration_id_map rows, zero owner-local orphan references, and zero UUID-shadow issues in both owner databases. The script now accepts IAM_PROFILE_LIVE_VALIDATION_RUN_REPORT plus IAM_PROFILE_LIVE_VALIDATION_REQUIRE_RUN_REPORT=1 to validate captured migrator run --service iam-profile-backfill --confirm JSON and require all postload-reconciliation items to have matched=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 validation status=passed, IAM/Profile base rows 4/4, migration-id map rows 63/17, orphan references 0/0, 22 matched postload-reconciliation items from the required run report, UUID readiness rows 81/39 with no blocking issues, UUID FK constraints 26/10 with no unvalidated constraints, and promoted UUID PK metadata for 21/9 IAM/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-validation validates the UUID foreign-key guard logic. Against an already-migrated target, IAM_PROFILE_UUID_FK_VALIDATION_CONFIRM=uuid-foreign-key-validation validates the owner-local NOT VALID IAM/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-promotion validates the UUID primary-key promotion guard logic. Against an already-migrated target, IAM_PROFILE_UUID_PK_PROMOTION_CONFIRM=uuid-primary-key-promotion proves 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.sql and 000006_profile_uuid_shadow_columns.sql. They add UUID shadow columns, legacy text id preservation, reference UUID backfill, unique/reference indexes, and iam_uuid_shadow_validation / profile_uuid_shadow_validation without changing current text primary keys or OpenAPI string ids.
  • Added UUID shadow dual-write migrations: 000010_iam_uuid_shadow_dual_write.sql and 000007_profile_uuid_shadow_dual_write.sql. They add owner-database triggers so repository writes maintain id_uuid, legacy_text_id, Profile users.account_id, and internal reference UUID columns while public service interfaces keep string ids. IAM audit/security validation now ignores null optional account_id values and still flags non-null account references that miss account_id_uuid.
  • Repository integration proof now includes TestPostgresIAMUUIDShadowDualWriteIntegration and TestPostgresProfileUUIDShadowDualWriteIntegration, 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. The iam_uuid_primary_key_readiness and profile_uuid_primary_key_readiness views plus make test-iam-profile-uuid-primary-key-readiness now provide the read-only UUID PK candidate gate. The iam_uuid_foreign_key_constraints and profile_uuid_foreign_key_constraints views plus make test-iam-profile-uuid-foreign-key-validation validate the owner-local UUID FK constraint set. The iam_uuid_primary_key_constraints and profile_uuid_primary_key_constraints views plus make test-iam-profile-uuid-primary-key-promotion prove 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-compose validates the live-smoke script fixtures. The live proof command is IAM_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_migrations rows for each IAM/Profile SQL file, Postgres health, service /healthz//readyz, and IAM JWKS, then tears the project down with volumes unless IAM_PROFILE_CLEAN_COMPOSE_KEEP=1 is set. It uses default host ports 5433, 8081, and 8082, or the IAM_PROFILE_HOST_POSTGRES_PORT, IAM_PROFILE_HOST_IAM_PORT, and IAM_PROFILE_HOST_PROFILE_PORT overrides for occupied developer machines. When IAM/Profile service images already exist locally, set IAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1 to use IAM_PROFILE_IAM_IMAGE, IAM_PROFILE_PROFILE_IMAGE, and a generated IAM_PROFILE_MIGRATOR_IMAGE with Docker Compose --no-build; set IAM_PROFILE_REBUILD_MIGRATOR_IMAGE=1 after migration changes.
  • IAM_PROFILE_K8S_SELF_TEST=1 make test-iam-profile-local-k8s validates the local-K8s smoke script render path. The live proof command is IAM_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 with hoctapaz/migrator:local, checks schema_migrations inside iam-db and profile-db, restarts the IAM/Profile deployments, verifies /healthz, /readyz, and IAM JWKS through kubectl 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 unless IAM_PROFILE_K8S_ALLOW_REMOTE=1 is 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. Rebuilt hoctapaz/iam-service:local, hoctapaz/profile-service:local, and hoctapaz/migrator:local were loaded into the kind node, then IAM_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-k8s ran through a temporary kubeconfig set to kind-hoctapaz-go. It applied the isolated namespace, completed migrate-iam-service and migrate-profile-service, verified service-owned schema_migrations counts 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, and IAM_PROFILE_HOST_PROFILE_PORT=58082. It rebuilt IAM/Profile/migrator images for Compose project hoctapaz-iam-profile-smoke, verified IAM/Profile migration completion and per-file schema_migrations rows 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-build because the build-mode path was waiting on external golang:1.22 image metadata. The passing run used IAM_PROFILE_CLEAN_COMPOSE_USE_LOCAL_IMAGES=1, hoctapaz/iam-service:local, hoctapaz/profile-service:local, hoctapaz/migrator:local, host ports 55435, 18081, and 18082, and wrote artifacts under output/qa/iam-profile-clean-compose-20260709-114124/. It verified IAM/Profile migration containers exited 0, service /healthz//readyz, and IAM JWKS. The QA trace is documented in IAM/Profile Clean Compose Smoke.
  • make test-runtime-foundation
  • pnpm docs:build
  • git 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 TEXT id 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-Service plus the shared X-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-service organization-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 by test-iam-profile-clean-compose with latest local-image artifacts under output/qa/iam-profile-clean-compose-20260709-114124/, and local K8s live proof is captured by test-iam-profile-local-k8s. Current default-route rollback proof is captured by test-iam-profile-route-rollback, with latest live route-header artifacts under output/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, confirmed migration_runs checkpoint writer, base legacy User row 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 under output/qa/iam-profile-target-validation-20260709-114607/.

Go-platform documentation is generated from repository Markdown.