chore: Migrate user identity from auth0_sub to MVP-generated UUID #206
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Migrate all database foreign keys from
auth0_sub VARCHAR(255)touser_profiles.id UUIDas the primary user identifier. Currently, MotoVaultPro has no universal internal user ID - all tables reference users via Auth0's external ID format (auth0|xxx). Theuser_profiles.idUUID column already exists but is unused as a relational key.Related Issues
Background
Current State
Every feature table uses
user_id VARCHAR(255)storing the Auth0 ID. This means:Affected Tables (10-15 tables)
subscriptions(user_id)donations(user_id)tier_vehicle_selections(user_id)vehicles(user_id)fuel_logs(user_id)documents(user_id)maintenancerecords (user_id)audit_logs(user_id)admin_users(auth0_sub)user_preferences(user_id)terms_agreements(user_id)backup_*tables (user_id)user_export/importtables (user_id)Auth Flow After Migration
auth0_subremains inuser_profilesas a login/lookup column but is no longer used as a foreign key.Proposed Migration Strategy
Phase 1: Add UUID columns
Phase 2: Populate from join
Phase 3: Add constraints
Phase 4: Update application code
userContext.userId = UUIDPhase 5: Drop old columns
Risk Assessment
Acceptance Criteria
user_profiles.idUUID instead ofauth0_subauth0_subremains inuser_profilesas login lookup columnuserContext.userIdDecision Critic Notes
This issue was separated from the subscription refactor (#205) based on Decision Critic analysis:
Out of Scope
Plan: Migrate user identity from auth0_sub to UUID
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Overview
Migrate all database foreign keys from
auth0_sub VARCHAR(255)touser_profiles.id UUIDas the primary user identifier. Theuser_profiles.idUUID column already exists but is unused as a relational key. The auth plugin already fetches the profile (with UUID) on every request -- the core change is usingprofile.idasuserContext.userIdinstead of the raw JWTsubclaim. After migration, all feature tables retain theuser_idcolumn name but with UUID type, so most repository SQL queries require no code changes.Approach: Multi-phase SQL migration (add columns, backfill, add constraints, drop old, rename) in a single deployment, combined with application code updates to the auth plugin, admin system, and cross-table joins.
Planning Context
Decision Log
idUUID PK -> renaming user_profile_id to user_id would create ambiguity between admin's own id and the user reference -> user_profile_id explicitly communicates the FK relationship -> all other tables rename because they have no conflicting id columnRejected Alternatives
Constraints and Assumptions
uuid_generate_v4()already available (used by user_profiles)backend/src/_system/migrations/run-all.tsexecutes per-featureauth0_submust remain inuser_profilesfor JWT resolutionnpm test,npm run lint,npm run type-checkmust all pass<default-conventions domain="testing">integration tests preferredKnown Risks
admin.controller.ts:206generates fake auth0_subuser-profile.repository.ts:248v.user_id = up.auth0_subrequest.user.subdirectly, notuserContext.userIdauth.plugin.ts:140auth0ManagementClient.getUser(userId)-- userId here is still from JWT sub before the reassignmentInvisible Knowledge
Architecture
Invariants
userContext.userIdis always a UUID after auth plugin hydrationauth0_subis only used for JWT-to-profile resolution (never as FK)admin_usersmust have a correspondinguser_profilesentryrequest.user.sub(raw JWT claim) is distinct fromuserContext.userId(UUID)admin_userskeepsuser_profile_idcolumn name (not renamed touser_id)admin_audit_logs.actor_admin_idandtarget_admin_idreferenceadmin_users.idUUIDTradeoffs
Milestones
Milestone 1: Database migration SQL
Files:
backend/src/features/user-profile/migrations/005_migrate_user_id_to_uuid.sql(new)Requirements:
user_profile_id UUIDcolumn to all 18 affected feature tablesuser_profilesjoin (SET user_profile_id = up.id FROM user_profiles up WHERE table.user_id = up.auth0_sub)admin_users: addid UUID PK DEFAULT uuid_generate_v4(), adduser_profile_id UUID, backfill from user_profiles join, create user_profiles entries for admins without one. Foradmin_audit_logs: changeactor_admin_idandtarget_admin_idfrom VARCHAR to UUID, backfill from admin_users join (SET actor_admin_id = au.id FROM admin_users au WHERE logs.actor_admin_id = au.auth0_sub)user_profiles(id)with ON DELETE CASCADE, add FK fromadmin_users.user_profile_idtouser_profiles(id), add indexes replacing old VARCHAR indexesuser_idcolumns and FK constraints from feature tables, renameuser_profile_idtouser_idin feature tables. Foradmin_users: drop oldauth0_subPK, keepuser_profile_idcolumn name (not renamed). Drop old auth0_sub columns fromadmin_audit_logsactor/target.community_stations.submitted_by,backup_history.created_by,station_removal_reports.reported_by-- add UUID equivalents, backfill, renameAcceptance Criteria:
user_id UUIDreferencinguser_profiles.idadmin_usershasid UUID PK+user_profile_id UUID FK(not renamed)admin_audit_logs.actor_admin_idandtarget_admin_idare UUID type referencingadmin_users.iduser_profiles.auth0_subremains as VARCHAR UNIQUE columnTests:
Milestone 2: Auth plugin and admin guard
Files:
backend/src/core/plugins/auth.plugin.tsbackend/src/core/plugins/admin-guard.plugin.tsRequirements:
getOrCreate(line 158), useprofile.id(UUID) asuserContext.userIdinstead of raw JWTsubrequest.user.subfor Auth0 Management API calls (line 140) -- this is the raw auth0_sub needed by Auth0WHERE auth0_sub = $1toWHERE user_profile_id = $1idinstead ofauth0_subAcceptance Criteria:
userContext.userIdcontains UUID format after authenticationTests:
backend/src/features/auth/tests/userContext.userIdas UUIDCode Changes:
Rename userId to auth0Sub for JWT sub, declare userId for UUID assignment:
Auth0 Management API uses auth0Sub:
Profile getOrCreate uses auth0Sub, then assigns UUID to userId:
Profile sync methods use auth0Sub:
Admin guard queries by user_profile_id:
Milestone 3: Admin system refactor
Files:
backend/src/features/admin/domain/admin.types.tsbackend/src/features/admin/data/admin.repository.tsbackend/src/features/admin/domain/admin.service.tsbackend/src/features/admin/api/admin.controller.tsRequirements:
auth0Sub: stringwithid: string+userProfileId: stringinAdminUser. UpdateRevokeAdminRequest,ReinstateAdminRequest,BulkRevokeAdminRequest,BulkReinstateAdminRequestto useidinstead ofauth0Sub.auth0_subtoid/user_profile_id. UpdatemapRowToAdminUserto mapidanduser_profile_id. UpdatemapRowToAuditLogto mapactor_admin_idandtarget_admin_idas UUID strings. ChangegetAdminByAuth0SubtogetAdminByUserProfileId. UpdatecreateAdminto acceptuserProfileId UUIDinstead ofauth0Sub. UpdatelogAuditActionto accept UUIDactorAdminIdandtargetAdminId.auth0|email_at_domain). Admin creation looks upuser_profilesby email to get UUID. If no profile exists, return error.Acceptance Criteria:
Tests:
backend/src/features/admin/tests/unit/admin.service.test.ts,backend/src/features/admin/tests/integration/admin.integration.test.tsMilestone 4: User profile repository refactor
Files:
backend/src/features/user-profile/data/user-profile.repository.tsFlags: needs conformance check (15+ methods changing parameter type)
Requirements:
getByAuth0Sub(auth0Sub)for auth plugin usage (JWT resolution)getById(id: string)method for UUID-based lookupsauth0Subparameter to accept UUIDid:update,updateSubscriptionTier,deactivateUser,reactivateUser,adminUpdateProfile,updateEmailVerified,markOnboardingComplete,updateEmail,requestDeletion,cancelDeletion,hardDeleteUser,getUserWithAdminStatus,getUserVehiclesForAdminWHERE auth0_sub = $1toWHERE id = $1hardDeleteUserto query by UUID across all tables (7 DELETE statements useWHERE user_id = $1with UUID value)listAllUserscross-table joins:v.user_id = up.id(wasup.auth0_sub),au.user_profile_id = up.id(wasau.auth0_sub)getUserWithAdminStatusjoin:au.user_profile_id = up.id(wasau.auth0_sub)Acceptance Criteria:
getByAuth0Substill works for auth pluginhardDeleteUserdeletes all user data correctlylistAllUsersjoins work with UUID columnsgetUserWithAdminStatusjoins with new admin_users schemaTests:
Milestone 5: Feature repository validation
Files:
backend/src/features/vehicles/data/vehicles.repository.tsbackend/src/features/fuel-logs/data/fuel-logs.repository.tsbackend/src/features/maintenance/data/maintenance.repository.tsbackend/src/features/documents/data/documents.repository.tsbackend/src/features/stations/data/stations.repository.tsbackend/src/features/subscriptions/data/subscriptions.repository.tsbackend/src/features/notifications/data/notifications.repository.tsbackend/src/features/ownership-costs/data/ownership-costs.repository.tsbackend/src/features/terms-agreement/data/terms-agreement.repository.tsbackend/src/core/user-preferences/data/user-preferences.repository.tsbackend/src/features/email-ingestion/data/email-ingestion.repository.tsRequirements:
user_id(renamed fromuser_profile_id), soWHERE user_id = $1queries andmapRow()functions require NO changesAcceptance Criteria:
mapRow()functions return correct userId values (UUID strings)Tests:
Milestone 6: Supporting code updates
Files:
backend/src/features/audit-log/data/audit-log.repository.tsbackend/src/features/backup/data/backup.repository.tsbackend/src/features/stations/data/community-stations.repository.tsbackend/src/features/user-import/domain/user-import.service.tsbackend/src/features/ocr/api/ocr.controller.tsRequirements:
LEFT JOIN user_profiles up ON al.user_id = up.auth0_subtoal.user_id = up.idcreated_bycolumn type changed by migration; mapRow stays the samesubmitted_bycolumn type changed by migration; verify queries work with UUIDuser_id = $1-- verify UUID compatibility. Update any direct references to auth0_sub format.request.userContext?.userIdfor user identification (7 endpoints) -- consistent with all other feature controllersAcceptance Criteria:
Tests:
Milestone 7: Test fixtures and documentation
Files:
backend/src/features/auth/tests/integration/auth.integration.test.tsbackend/src/features/auth/tests/unit/auth.service.test.tsbackend/src/features/admin/tests/integration/admin.integration.test.tsbackend/src/features/admin/tests/unit/admin.service.test.tsbackend/src/features/maintenance/tests/fixtures/maintenance.fixtures.jsondocs/PLATFORM-SERVICES.md(if user identity documented)Requirements:
auth0|...values with UUID format in test fixtures and mocksnpm test,npm run lint,npm run type-checkAcceptance Criteria:
auth0|format in test code (except for auth plugin tests that test JWT parsing)Tests:
Milestone Dependencies
M1 must complete first. M2 depends on M1. M3 and M4 depend on M2. M5 and M6 depend on M1. M7 depends on all.
Verdict: AWAITING_REVIEW | Next: Plan review cycle (QR code -> QR docs)
Milestone: M1 - Database migration SQL
Phase: Execution | Agent: Platform | Status: PASS
Completed
backend/src/core/identity-migration/migrations/001_migrate_user_id_to_uuid.sqlcore/identity-migrationto end of MIGRATION_ORDER in run-all.ts to ensure all feature tables exist before migration runsDeviation from Plan
core/identity-migration/migrations/001_migrate_user_id_to_uuid.sqlinstead ofuser-profile/migrations/005_*to avoid ordering issue: user-profile runs 3rd in MIGRATION_ORDER but migration needs all tables (subscriptions runs 16th)Verdict: PASS | Next: M2 (auth plugin + admin guard)
Session 2 Progress Update
Branch:
issue-206-migrate-user-identity-uuidCommitted
60118881321440fd9d1adb418a50Uncommitted (5 files staged for M6)
Files modified but NOT committed:
audit-log/data/audit-log.repository.ts- JOIN changed fromup.auth0_subtoup.idocr/api/ocr.controller.ts- All 7(request as any).user?.subchanged torequest.userContext?.userIduser-profile/api/user-profile.controller.ts- Renamed auth0Sub vars to userId, usesgetById()instead ofgetOrCreateProfile()user-profile/data/user-profile.repository.ts- Fixedau.auth0_sub as admin_auth0_subtoau.id as admin_idin admin JOIN queries (2 places)user-profile/domain/user-profile.service.ts- All admin-focused methods now accept userId (UUID), usegetById()instead ofgetByAuth0Sub()Remaining Work
subscriptions.service.tsadminOverrideTieralready accepts UUID (it does - confirmed). Checkuser-import.service.tsfor any auth0_sub refs.auth0|xxxwith UUID format), runnpm run type-check,npm run lint,npm test. Update docs if needed.:auth0SubURL params (routes changed to:idfor admins,:userIdfor users).Session 3 Progress Update
Branch:
issue-206-migrate-user-identity-uuidAll Milestones Complete
60118881321440fd9d1adb418a503b1112a754639cM6 Details (committed this session)
audit-log.repository.ts: JOIN onuser_profiles.idinstead ofauth0_subbackup.controller.ts: UseuserContext.userIdinstead ofauth0Sub(was undefined post-migration)ocr.controller.ts: All 7 endpoints userequest.userContext.userIdinstead of(request as any).user.subuser-profile.controller.ts: UsegetById()with UUID, removedgetOrCreateProfile()callsuser-profile.service.ts: All admin methods accept UUIDuserIdparameteruser-profile.repository.ts: Fixed admin JOIN aliases fromauth0_subtoidM7 Details (committed this session)
Backend test fixtures (11 files):
auth0|xxxtest user IDs replaced with UUID formatid/userProfileIdschemadeletionRequestedAt/deletionScheduledForfieldsapp.serverusageFrontend (9 files):
AdminUsertype:auth0Sub->id+userProfileIdadmin.api.ts: All user management methods useuserId(UUID) params, admin revoke/reinstate useiduseUsers.ts/useAdmins.ts: All mutation hooks useuserId/idinstead ofauth0SubAdminUsersPage.tsx+AdminUsersMobileScreen.tsx:user.auth0Sub->user.idencodeURIComponent()(UUIDs don't need URL encoding)Validation
tsc --noEmitwith zero errorsReady for PR
All 7 milestones are complete. Branch has 7 commits ready for PR to main.