6.5 KiB
Identity / Employee Separation Review
1. Summary
This change starts the separation between login identity (users) and employee master data (employees) without breaking the existing Auth.js credential login flow.
The implementation is intentionally staged:
- keep
usersas the current authentication source - introduce additive fields needed for identity separation
- start linking
users.employeeId -> employees.id - backfill
training_records.employee_id - backfill
employee_training_targets.employee_id - change employee import to update
employeesfirst, then link users
2. Architecture Changes
Users
users remains the authentication identity table for now, but now also stores identity-specific metadata:
providerproviderUserIdemployeeIdlastLoginAt
Employees
employees becomes the HR master source and now supports richer profile fields:
prefixfirstNameThlastNameThfirstNameEnlastNameEndisplayNamephonecompanyNamestatus
Linking
User-to-employee linking now uses employeeCode within the active organization.
New helper:
src/features/employees/server/employee-identity-linking.ts
This helper:
- upserts employee master rows from import
- links
users.employeeId - backfills
training_records.employee_id - backfills
employee_training_targets.employee_id
3. Database Changes
Migration file:
drizzle/0009_lean_hobgoblin.sql
Schema changes:
users.provider text not null default 'credentials'users.provider_user_id text nullusers.employee_id integer nullusers.last_login_at timestamptz nullemployees.prefix text nullemployees.first_name_th text nullemployees.last_name_th text nullemployees.first_name_en text nullemployees.last_name_en text nullemployees.display_name text nullemployees.phone text nullemployees.company_name text nullemployees.status text not null default 'active'employee_training_targets.employee_id integer null
Backfill in migration:
employees.display_namefromfull_nameemployees.company_namefromcompanyemployees.statusfromis_activeusers.employee_idfrom membership +employee_codetraining_records.employee_idfrom linked usersemployee_training_targets.employee_idfrom linked users
4. Migration Strategy
This is a safe additive migration.
- Add new nullable columns and defaults.
- Backfill employee links from existing
employee_code. - Keep all old columns and old flows working.
- Start writing
employeeIdin import/auth/training-record paths. - Delay destructive cleanup until reports/dashboard/training matrix fully move to employee-master ownership.
5. User Login Behavior
Credential login is unchanged for end users.
New behavior on successful login:
- set
users.provider = 'credentials' - set
users.last_login_at - try to link
users.employeeIdfrom the active organization andemployeeCode
If no matching employee master exists:
- login still succeeds
employeeIdremainsnull
6. Keycloak Mapping Behavior
Keycloak provider is not implemented in this stage.
However, the schema and identity model now support it:
providerproviderUserIdemployeeId
Recommended future behavior:
- read
empcode - map to
employees.employeeCode - set
users.employeeId
7. Employee Import Behavior
Employee import now does more than create/update user rows.
It now:
- upserts into
employees - links any existing users in the same organization by
employeeCode - keeps updating/creating user rows for backward compatibility
- stores
employee_training_targets.employee_id
Important note:
- this stage keeps the current auto-create user behavior because the existing application still depends on
usersin many places
8. Training Record Impact
New and edited training records now save:
userIdas the authenticated/persona owner used by current flowsemployeeIdfrom the linked employee master when available
This means we can start moving reports and dashboards toward employee ownership without losing compatibility.
9. Dashboard Impact
Dashboard queries are not fully migrated yet.
What changed now:
- target resolution can read employee-linked targets indirectly through the user-to-employee link path
What remains:
- overview aggregates still primarily scope through
usersandtraining_records.userId
10. Reports Impact
Reports are not fully converted in this stage.
Current state:
- report ownership and filtering still use
users - training records now carry
employeeId, which prepares the next migration stage
11. Permission Rules
No role behavior changed in this stage.
super_admin: unchangedadmin: unchangeduser: unchanged
This stage is structural, not RBAC-changing.
12. Manual Test Checklist
- Run migration
0009_lean_hobgoblin.sql. - Sign in with an existing credential user.
- Verify
users.last_login_atupdates. - Verify
users.employee_idlinks whenemployee_codematches an employee master row in the active organization. - Import employee Excel data with existing employee codes.
- Verify
employeesrows are created/updated. - Verify matching
users.employee_idis linked. - Create a training record.
- Verify
training_records.employee_idis saved when the user is linked. - Update a training record.
- Verify
employee_training_targets.employee_idis populated on imported targets.
13. Rollback Plan
If rollback is required:
- stop app deployment
- revert application code
- keep added columns unused, or manually drop them in a dedicated rollback migration
Because this stage is additive, the safest rollback is application-level rollback first, not destructive DB rollback.
14. Known Limitations
usersstill contains employee-facing profile fields for compatibility- reports still aggregate mainly via
userId - dashboard still aggregates mainly via
userId - training matrix still aggregates mainly via
userId - import still auto-creates login users for compatibility
- Keycloak runtime mapping is not implemented yet
Changed Files
src/db/schema.tssrc/auth.tssrc/types/next-auth.d.tssrc/features/employees/server/employee-identity-linking.tssrc/app/api/import-employees/route.tssrc/features/users/server/user-data.tssrc/app/api/training-records/route.tssrc/app/api/training-records/[id]/route.tssrc/features/training-policy/server/employee-training-target-data.tsdrizzle/0009_lean_hobgoblin.sql