Org domain decisions
This page records the ratified design decisions for the Org domain. Read it before you design anything in this domain.
Each decision states what we decided, why, and where the evidence is. Decisions live only on this page. Other docs must link here instead of repeating the text. When a decision changes, update this page the same day.
Schema truth is jodapp-api/docs/db/org.dbml. Verify columns there before you rely on this page.
D1 — is_selected always marks the membership Rails acts as (2026-09-24)
An Org::Membership connects one Identities::User to one Org::Company. A person can hold several, one per company. The column org_memberships.is_selected marks the one the person acts as. A partial unique index, index_org_memberships_on_user_id_where_is_selected, allows at most one selected row per person.
A membership is usable when its status is active and its company's status is active. Org::Membership#usable? answers this. Org::Memberships::CurrentResolveService answers which membership Rails acts as:
- the selected row, but only while it is usable
- otherwise the newest usable row
- when no row is usable, the newest row
Every employer request asks Org::Memberships::CurrentResolveService, through Employers::BaseController.
Ali decided this on 2026-09-24.
What we decided
| Part | Decision | Ruled |
|---|---|---|
| The rule | While a person has a usable membership, is_selected: true sits on the row Org::Memberships::CurrentResolveService answers. Every other row of that person has is_selected: false | 2026-09-24 |
| When the selected membership stops being usable | The write that makes it unusable moves is_selected to the row Org::Memberships::CurrentResolveService answers, in the same transaction. Two writes do this: a Jod team member changes a company's status in Team Portal, and the nightly JodGig company sync copies a company's status. A write that revokes a member does the same | 2026-09-24 |
| When no usable membership is left | The same write sets is_selected: false on every row of the person. The person then lands on /employers/access-unavailable, which tells them they have no company they can use. JodApp Web — What the access-unavailable page shows states the text | 2026-09-24 |
| A company that is switched on again | It does not get the selection back when the person already acts as another usable membership. Churned companies rarely return, so this costs little. A person with no selected row gets one from the same write | 2026-09-24 |
| Who reads it | GET /identities/users/current/org_memberships returns is_selected on every entry. JodApp Web reads it there, and nowhere else | 2026-09-24 |
| How the rule runs | One call to the service in every place that writes a company's status or a membership's status: the managers, and the JodGig company sync. It is never a model callback, because the codebase makes every side effect an explicit call in the code that does the write | 2026-09-24 |
Why
- One meaning for one flag. JodApp Web reads
is_selected: trueas "Rails acts as this membership". It never works out which membership is current in the browser. - The session does not answer it. Identities D6 — The current-session answer holds the person and one access word per area keeps membership ids out of the session. So the memberships endpoint gives the only answer, and it must be right.
- The same transaction, so no request sees a half-done change. A request that ran between two separate writes would read a selected row on a company that is already switched off.
- Clear, not keep, when nothing is usable. A kept flag on an unusable row would give
is_selected: truea second meaning. Every reader would then have to checkaccess_statusas well, and the row marked selected could differ from the rowOrg::Memberships::CurrentResolveServiceanswers.
Evidence
| Fact | Where |
|---|---|
| The column and the index | is_default became is_selected in jodapp-api#1926, migration 20260821101500, merged 2026-08-21. The partial unique index came in jodapp-api#1915, commit 85d316953, migration 20260822090000, merged 2026-08-23 |
| The rule that picks the row | jodapp-api at bc04d829b, app/domains/org/memberships/current_resolve_service.rb:63-75 |
| A switch refuses an unusable row, and clears then sets in one transaction | app/domains/org/memberships/employers_select_validator.rb:24-40; employers_select_manager.rb:63-81 |
A company switched off in Team Portal left is_selected where it was | app/domains/org/companies/team_update_manager.rb:59, :64-74 |
The nightly JodGig company sync changes status with upsert_all, which runs no model code | app/domains/jod_gig/companies/sync_service.rb:47-63 |
| No model callbacks for side effects | jodapp-api AGENTS.md:194 |
| Production on 2026-09-24 | 78 memberships and 78 people, each with one membership, and each membership selected. 0 people with two memberships. 2 selected memberships are revoked on an active company, so the rule clears 2 and moves 0. org_companies.status: 228 active, 144 disabled, 0 empty. Counted by Ali in the Rails console |
| The first people with two memberships | They arrive with the JodGig employer sync. JodGig holds 1,349 active manager accounts, and 248 of them sit on a disabled or deleted company. Counted on a scrubbed copy of the JodGig database, 2026-09-24. The sync's own plan already moves the flag on a revoke: Employer migration phase 3 — Maintain one selected membership |