Phase 1: make JodApp understand the migrated access
This phase changes no JodGig data and runs no employer migration. It prepares JodApp to interpret the future identity, membership and outlet-assignment rows correctly.
Outcome
Before importing access, prove that JodApp can select, switch and authorize through one active membership out of many.
At the end of this phase:
- one identity can have several company memberships;
- an employer request always uses one explicitly selected active membership;
- revoked and suspended memberships grant no access;
- a user can switch among their active memberships;
- JodGig-created and JodApp-created memberships can coexist safely;
- JodGig
LOCATIONuses the destination nameoutlet_manager; - migrated users cannot change JodGig-owned identity fields in JodApp; and
- no launch behaviour depends on
is_owner.
Change the identity association
| Current assumption | Required model |
|---|---|
| One identity has one organisation membership | One identity has many organisation memberships |
| Any membership row is enough for employer access | The selected membership must be active |
| The request path finds an arbitrary membership | The request path resolves an explicit or default active membership |
The intended association is:
class Identities::User
has_many :org_memberships,
class_name: "Org::Membership",
foreign_key: :user_id
end
Keep candidate behaviour separate through its candidate or talent profile. A person who can apply for jobs and represent a company still has one identity.
Select the current membership
A membership ID, not a company ID supplied by itself, establishes the employer context.
Every employer request must resolve a membership that:
- belongs to the authenticated identity;
- has
status = active; - belongs to an active company; and
- is permitted for the requested employer operation.
The frontend may remember the selected membership between requests. The server must still validate it every time. A client-supplied company ID must never bypass the membership check.
When no membership has been selected:
- use the active membership marked
is_default; - if corrupt data contains no active default, return an explicit account-setup error and report it;
- do not silently choose an arbitrary database row.
The reconciliation is responsible for maintaining one valid default, but the request path must fail safely when that promise is broken.
Support company switching
Switching changes the current membership; it does not create access and does not change ownership.
Provide an endpoint that returns the identity's active memberships with enough company information for a switcher. Selecting another membership changes the current employer context only after the server confirms that it belongs to the identity and remains active.
Switching companies does not:
- add a membership;
- change a role;
- set
is_owner; - change JodGig's company grants; or
- send data back to JodGig.
Changing the default company in JodApp may be supported later. For this migration, the sync maintains a valid default and the user can switch during normal use.
Record membership provenance
The reconciliation needs to know which memberships it is allowed to update or revoke.
Add an explicit source to org_memberships:
provisioning_source
jodgig
jodapp
The exact database representation may follow the existing enum convention, but the values and behaviour are part of the contract:
| Source | Created by | May employer reconciliation update or revoke it? |
|---|---|---|
jodgig | Employer reconciliation | Yes |
jodapp | Invitation, admin or another native flow | No |
Do not copy remote_gig_user_id to memberships. That ID identifies the person and remains on Identities::User; it does not say whether a particular membership came from JodGig.
Safe schema rollout
| Step | Check |
|---|---|
| Add the source column without guessing historical ownership | Existing reads and writes continue safely |
| Audit existing memberships | Identify whether any were produced by earlier employer-sync experiments |
Backfill proven native rows as jodapp | Do not classify an uncertain row silently |
Backfill any proven legacy rows as jodgig | Record the evidence used |
| Make all membership creation paths write a source | New rows cannot be ambiguous |
| Add the final presence constraint | Only after the audit finds no unknown rows |
The current employer membership PR has not been accepted, so the expected result is that existing production memberships are native. Verify that with data rather than relying on the expectation.
Use the destination role language
| JodGig term | JodApp membership role |
|---|---|
HQ | hq_manager |
AREA | area_manager |
LOCATION | outlet_manager |
SUPER_HQ_EXTERNAL | hq_manager |
JodApp renamed legacy locations to outlets. Do not add or retain a location_manager role for migrated employers.
The current jodapp.org_memberships.role column stores string values, including location_manager. Before renaming it, count existing rows where jodapp.org_memberships.role = location_manager. Migrate those rows to outlet_manager, update the Rails enum to accept outlet_manager, and verify that no stored location_manager values remain.
Enforce one-way identity ownership
A migrated identity displays JodGig-owned fields in JodApp but edits them in JodGig.
An identity with remote_gig_user_id is linked to JodGig. JodApp account endpoints must reject changes to the copied source-owned fields, including email and password. The frontend should remove or redirect those controls, but server enforcement is required.
This restriction applies to the fields copied by the employer reconciliation. It does not prevent the identity from:
- switching memberships;
- using Careers and other JodApp features;
- receiving a native JodApp membership; or
- changing data that exists only in JodApp.
There is no reverse synchronization.
Treat status as membership access
| Source event | JodApp result |
|---|---|
| JodGig employer disabled or deleted | Revoke JodGig-provisioned memberships |
| Company disabled or removed | Revoke the corresponding JodGig-provisioned membership |
| Native JodApp membership still active | Leave it active |
| Candidate profile exists | Leave candidate access unchanged |
Do not globally deactivate Identities::User merely because their JodGig employer access was removed. One identity may also be a candidate or hold a native membership. Employer authorization must depend on an active membership.
Leave ownership out
Access to a company does not prove ownership of that company.
Set is_owner = false on migrated memberships. No launch behaviour may require this flag.
If a future billing or administration feature needs company ownership, define it from that feature's business rules and calculate it separately with all company members available.
Acceptance tests
| Scenario | Expected result |
|---|---|
| Identity has two active memberships | Both are listed and either can be selected |
| Identity selects another person's membership ID | Request is rejected |
| Selected membership is revoked between requests | Next request is rejected |
| Identity has an active default | Request with no prior selection resolves that membership |
| Identity has no active default because of corrupt data | Explicit setup error; no arbitrary membership chosen |
| Native and JodGig memberships coexist | Both can be active and retain different provenance |
| Migrated identity changes email or password through JodApp | Server rejects the change |
| Migrated employer is disabled but has a candidate profile | Employer access stops; candidate identity is not globally disabled |
Migrated role is LOCATION | JodApp stores and authorizes outlet_manager |
| Migrated membership is inspected | is_owner remains false |
Phase completion gate
Do not start writing migrated memberships until the application passes the many-membership and revoked-access tests.
This phase is complete when one manually created identity with several memberships can choose which allowed company they are currently working in, and every employer endpoint consistently rejects a revoked selected membership.