Skip to main content

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

TL;DR

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 LOCATION uses the destination name outlet_manager;
  • migrated users cannot change JodGig-owned identity fields in JodApp; and
  • no launch behaviour depends on is_owner.

Change the identity association

Current assumptionRequired model
One identity has one organisation membershipOne identity has many organisation memberships
Any membership row is enough for employer accessThe selected membership must be active
The request path finds an arbitrary membershipThe 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

TL;DR

A membership ID, not a company ID supplied by itself, establishes the employer context.

Every employer request must resolve a membership that:

  1. belongs to the authenticated identity;
  2. has status = active;
  3. belongs to an active company; and
  4. 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

TL;DR

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

TL;DR

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:

SourceCreated byMay employer reconciliation update or revoke it?
jodgigEmployer reconciliationYes
jodappInvitation, admin or another native flowNo

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

StepCheck
Add the source column without guessing historical ownershipExisting reads and writes continue safely
Audit existing membershipsIdentify whether any were produced by earlier employer-sync experiments
Backfill proven native rows as jodappDo not classify an uncertain row silently
Backfill any proven legacy rows as jodgigRecord the evidence used
Make all membership creation paths write a sourceNew rows cannot be ambiguous
Add the final presence constraintOnly 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 termJodApp membership role
HQhq_manager
AREAarea_manager
LOCATIONoutlet_manager
SUPER_HQ_EXTERNALhq_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

TL;DR

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 eventJodApp result
JodGig employer disabled or deletedRevoke JodGig-provisioned memberships
Company disabled or removedRevoke the corresponding JodGig-provisioned membership
Native JodApp membership still activeLeave it active
Candidate profile existsLeave 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

TL;DR

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

ScenarioExpected result
Identity has two active membershipsBoth are listed and either can be selected
Identity selects another person's membership IDRequest is rejected
Selected membership is revoked between requestsNext request is rejected
Identity has an active defaultRequest with no prior selection resolves that membership
Identity has no active default because of corrupt dataExplicit setup error; no arbitrary membership chosen
Native and JodGig memberships coexistBoth can be active and retain different provenance
Migrated identity changes email or password through JodAppServer rejects the change
Migrated employer is disabled but has a candidate profileEmployer access stops; candidate identity is not globally disabled
Migrated role is LOCATIONJodApp stores and authorizes outlet_manager
Migrated membership is inspectedis_owner remains false

Phase completion gate

TL;DR

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.