Employer migration: the new plan
This is the implementation guide for giving existing JodGig employers access to JodApp while both systems remain live.
The plan starts with what an employer must be able to do. It then works backwards into application changes, migration rules, database writes, and rollout checks. The implementation is split into complete phases so each pull request proves a useful part of the journey.
The plan in one picture
| Decision | Choice |
|---|---|
| Source of truth | JodGig owns migrated employer identity and legacy access data. |
| Direction | Changes flow from JodGig to JodApp only. |
| Method | Calculate the complete intended result, then make JodApp agree with it. |
| Frequency | Run daily after the company and outlet syncs. Allow the same job to be started manually. |
| Size | Read all employer-type users. The population is small enough that a checkpoint adds risk without useful savings. |
The calculation step is deliberately separate from the writing step. The dry run and the live run must use the same calculation. Otherwise the report cannot prove what the live job will do.
Start with the business outcome
The migration is complete when correct employer access works and incorrect access does not. Creating rows alone is not success.
An eligible JodGig employer must be able to:
- log in to JodApp with the email and password currently managed in JodGig;
- enter every valid company they are allowed to manage;
- see only the outlets allowed by their role;
- choose which of their allowed companies they are currently working in when they have several memberships;
- lose legacy access after it is removed in JodGig; and
- keep any membership created directly in JodApp, even if JodGig does not know about it.
How each business outcome is delivered
This table is organized by business outcome, not by implementation order. Each row starts with something an employer or reviewer must be able to observe, then shows the work that makes it true. Phase links within a row are listed in implementation order. The complete ordered plan is in Delivery phases.
| Business outcome | Work that makes the outcome true | Main implementation phases, in order |
|---|---|---|
| Same login works | Prevent migrated users from editing JodGig-owned login fields in JodApp; match the correct identity; then copy and maintain the JodGig password representation safely. | Phase 1 → Phase 2 → Phase 3 |
| Correct companies | Calculate the complete membership set from current JodGig company relationships, then create, update or revoke those JodGig-provisioned memberships. | Phase 2 → Phase 3 |
| Correct outlet access | Establish the outlet_manager role and membership-based authorization; calculate the required outlet assignments; then make JodApp agree with that calculation. | Phase 1 → Phase 2 → Phase 3 |
| Choosing a company to work in | Let the employer select one active membership; calculate a valid initial default; then save that default without changing which companies they may access. | Phase 1 → Phase 2 → Phase 3 |
| Removed access stops working | Require an active membership on every employer request; detect company access that disappeared from JodGig; then revoke the corresponding JodGig-provisioned membership. | Phase 1 → Phase 2 → Phase 3 |
| Native JodApp access survives | Record whether JodGig or JodApp created each membership; include native memberships when deciding what to preserve; then update or revoke only JodGig-provisioned memberships. | Phase 1 → Phase 2 → Phase 3 |
| People can verify correctness | Produce the intended result without writing, review named examples and counts, prove that a second identical run makes no changes, and expose failures after launch. | Phase 2 → Phase 4 |
The records and what they mean
A person is an identity. Employer access is a membership. Outlet limits belong to that membership.
| JodApp record | Question it answers | Example |
|---|---|---|
Identities::User | Who is logging in? | “This is Aisyah.” |
Org::Membership | Which company may this person represent, and in what role? | “Aisyah is an HQ manager at Company A.” |
Org::OutletAssignment | Which outlets may this membership use? | “At Company A, Aisyah manages Orchard and Bugis.” |
One identity may have several memberships. This is required for external super-HQ users and for enterprise customers whose divisions are represented as separate JodApp companies.
There is no Identities::Employer. Employer is how a person acts for a company, so it is represented by Org::Membership.
Source ownership while both systems are live
JodGig owns migrated employer identity and legacy access. JodApp owns its native memberships and new product data.
| Information | Owner | Reconciliation rule |
|---|---|---|
| Migrated email, name, mobile, address and password | JodGig | Copy later JodGig changes to JodApp. Do not offer JodApp edit paths for migrated users. |
| Whether a legacy employer is active | JodGig | Revoke their JodGig-provisioned memberships when the source becomes inactive. |
| Legacy company and outlet access | JodGig | Make JodGig-provisioned access agree with the current source relationships. |
| Notification choices on a JodGig-provisioned membership | JodGig | Copy all three channels one way on every reconciliation. |
| Membership created directly in JodApp | JodApp | Never update or revoke it because of missing JodGig access. |
| Careers jobs and other new-product data | JodApp | Never send it back to JodGig through this reconciliation. |
Memberships therefore need explicit provenance, for example provisioning_source = jodgig | jodapp. The legacy person ID remains on Identities::User; it should not be copied to every membership.
Password compatibility exception
| Source password | JodApp password | Action |
|---|---|---|
| BCrypt | Any value | Copy the source BCrypt hash, adjusting its prefix for Ruby if needed. |
| Legacy format | Same legacy value | Keep it until the person logs in. |
| Unchanged legacy format | JodApp upgraded it to BCrypt after a successful login | Preserve the JodApp BCrypt value so the daily run does not undo the upgrade. |
| Source later changes to BCrypt | Locally upgraded or old value | Copy the new source hash. |
This is still one-way ownership. The exception prevents an unchanged old source representation from reversing a safe local format upgrade.
Who receives access
Ordinary employers use their live JodGig company. External super-HQ users use valid jodgig.user_company grants. Internal super-HQ is staff impersonation and is excluded.
| JodGig type | JodApp role | Company source | Outlet scope |
|---|---|---|---|
HQ | hq_manager | jodgig.users.company_id | Whole company |
AREA | area_manager | jodgig.users.company_id | Every live outlet assigned to that area user |
LOCATION | outlet_manager | jodgig.users.company_id | The matching live outlet |
SUPER_HQ_EXTERNAL | hq_manager | Valid live jodgig.user_company rows | Whole company for each membership |
SUPER_HQ_INTERNAL | None | Excluded | JOD staff impersonation is a separate future feature |
An ordinary employer is eligible when the user is enabled, not deleted, and their company is live, non-obsolete, and present in JodApp.
An external super-HQ identity is eligible when it is enabled, not deleted, and has at least one distinct valid company grant. The grant must not be deleted, its company must be live and non-obsolete, and that company must exist in JodApp.
For external super-HQ:
jodgig.user_companydetermines access;jodgig.users.company_idmay choose the default membership only when it is one of those valid grants;- otherwise the valid
jodgig.user_companyrow with the highest pivotidbecomes the default; - changing the default never changes the set of allowed companies; and
- every migrated membership has
is_owner = false.
The five decisions produced by the planner
Every source person receives one explainable decision before any data is written.
| Decision | Meaning |
|---|---|
migrate | Eligible source person has no linked JodApp identity yet. |
update | Eligible source person already exists and their JodGig-owned data or access may need to change. |
revoke | Previously migrated access is no longer present or eligible in JodGig. Keep historical identity data but stop legacy access. |
skip | Ineligible source person has never received JodApp access. Do not create them. |
needs_review | The program cannot safely choose, such as an email belonging to a different linked identity. Write nothing for that person. |
The daily job reads ineligible employer-type users too. Otherwise a person disabled after migration would disappear from the input while their old JodApp membership remained active.
Delivery phases
Make JodApp enforce the result first, calculate the complete result second, write it third, and schedule it only after verification.
- Application readiness — support many memberships, choosing which company the employer is currently working in, active-membership authorization, provenance, role naming, and source-owned edit restrictions.
- Desired-state planner — calculate the intended identity, memberships, outlet assignments, defaults and decisions without writing.
- Reconciliation writer — apply that plan safely and make repeated runs converge to the same result.
- Verification and rollout — test complete journeys, review a production dry run, run twice in QA, then schedule daily.
The reasons for replacing the existing implementation are documented separately in What went wrong previously.
Deliberately outside the launch plan
| Keep out | Reason |
|---|---|
| Employer migration during login | Immediate provisioning is not required, and login cannot repair a missing company or outlet. |
| Hourly incremental checkpoints | Changes to JodGig companies, company grants and locations do not reliably update jodgig.users.updated_at. |
| Owner selection | No current feature reads it, and legacy access is not evidence of company ownership. |
Old issue 5.1: create JodGig::Users::UpsertIdentitiesUserService, then change the scheduled talent sync, talent migration during login, and employer sync to use it | This is not required for employer migration. Keep the working talent writers unchanged while we build and verify the replacement employer reconciliation. |
| Mobile collection prompt | Useful later, but not required to establish correct employer access. |
| Login-page redesign | Product presentation should not block identity and access correctness. |
Definition of done
A repeat run changes nothing, and every employer request is authorized through one active selected membership.
The migration is ready for daily use only when:
- every eligible source person maps to exactly one identity;
- no new disabled, deleted, orphaned, or closed-company person is created;
- every active JodGig-provisioned membership corresponds to a current valid source grant;
- every removed source grant is revoked on the next run;
- native JodApp memberships are unchanged;
- every area or outlet manager has exactly the intended outlet assignments;
- every employer endpoint rejects revoked or suspended memberships;
- a multi-company user can choose any of their active memberships as the company they are currently working in;
- email, password and notification updates move only from JodGig to JodApp;
- all ambiguous identity matches are reported without partial writes; and
- running the same source snapshot twice produces no material changes on the second run.
Decision log
| Date | Decision |
|---|---|
| 2026-07-23 | Ownership: every migrated membership has is_owner = false. The business team collects owner information from clients and updates the column manually. The old HQ-first owner-selection rule is retired. |
| 2026-07-23 | Default membership for external super-HQ: the valid grant with the highest jodgig.user_company.id. Low risk — the frontend will provide a company picker. |
| 2026-07-23 | Role name is outlet_manager, consistent with Org::Outlet. The existing location_manager enum value and rows are renamed in Phase 1. |
| 2026-07-23 | Mobile collection stays out of the migration. Later, at first login, employers will be asked to add and OTP-verify their real mobile so they can receive notifications — optional, with per-channel notification choices. |