Skip to main content

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:

  1. the selected row, but only while it is usable
  2. otherwise the newest usable row
  3. 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​

PartDecisionRuled
The ruleWhile 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: false2026-09-24
When the selected membership stops being usableThe 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 same2026-09-24
When no usable membership is leftThe 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 text2026-09-24
A company that is switched on againIt 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 write2026-09-24
Who reads itGET /identities/users/current/org_memberships returns is_selected on every entry. JodApp Web reads it there, and nowhere else2026-09-24
How the rule runsOne 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 write2026-09-24

Why​

  1. One meaning for one flag. JodApp Web reads is_selected: true as "Rails acts as this membership". It never works out which membership is current in the browser.
  2. 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.
  3. 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.
  4. Clear, not keep, when nothing is usable. A kept flag on an unusable row would give is_selected: true a second meaning. Every reader would then have to check access_status as well, and the row marked selected could differ from the row Org::Memberships::CurrentResolveService answers.

Evidence​

FactWhere
The column and the indexis_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 rowjodapp-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 transactionapp/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 wasapp/domains/org/companies/team_update_manager.rb:59, :64-74
The nightly JodGig company sync changes status with upsert_all, which runs no model codeapp/domains/jod_gig/companies/sync_service.rb:47-63
No model callbacks for side effectsjodapp-api AGENTS.md:194
Production on 2026-09-2478 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 membershipsThey 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