Org::Membership
Purpose
An Org::Membership represents a user's access to a specific company on the Jod Platform. It links an Identities::User to an Org::Company, defines their role, and determines what they can see and do within that company.
A single user can have memberships in multiple companies. For example, an HR lead at a hotel group may have an hq_manager membership at three different hotel properties. They log in once and switch between companies from the UI.
Org::Membership replaces the concept of Org::UserProfile from the initial system design. While the talent side uses Talent::Profile to represent "who is this worker" (skills, experience, ratings), the employer side is fundamentally about access — which company, what role, which outlets. This is why the model is called Membership, not UserProfile.
Key Concepts
Roles
Each membership has a role that defines what the user can do. Roles have fixed permissions — there is no per-company permission customization.
| Role | Scope | Capabilities |
|---|---|---|
hq_manager | All outlets in the company | Full access: manage jobs, users, outlets, billing, credits, job templates |
area_manager | Assigned outlets only | Manage jobs and candidates at assigned outlets. View credit history. Cannot manage users or outlets. |
outlet_manager | Single assigned outlet | Manage jobs and candidates at their outlet. View credit history. Cannot manage users or outlets. |
Why three roles are sufficient: Analysis of the legacy system (JodGig) showed 277 granular permissions across 315 companies, but only 7 distinct permission sets. The differences between area and outlet managers are purely about scope (which outlets), not capabilities. Per-company permission customization was built in JodGig but barely used. See JodGig Org Statistics for the full analysis.
Outlet Assignment
area_manager and outlet_manager roles are scoped to specific outlets via Org::OutletAssignment:
- hq_manager: no outlet assignments needed — implicit access to all outlets in the company
- area_manager: assigned to N outlets (direct user-to-outlet assignment, not a named group)
- outlet_manager: assigned to exactly 1 outlet
This is a direct assignment model, not a persistent grouping model. There is no "area" or "zone" entity — an area manager simply has a set of outlet assignments. If the company reorganizes which outlets each manager oversees, the outlet assignments are updated directly.
Ownership
Each company has exactly one owner — the user who created the company (via self-serve sign-up) or was designated as owner during admin onboarding.
Ownership is a flag on the membership, not a separate role:
Org::Membership (role: hq_manager, is_owner: true)
The owner:
- Cannot be removed by other
hq_managermembers — only byIdentities::Adminor by transferring ownership - Can transfer ownership to another
hq_managerin the same company - Has the same permissions as any other
hq_manager— ownership is about protection from accidental removal, not additional capabilities
Multi-Company Access
A single Identities::User can have multiple memberships — one per company:
Identities::User (myname@hotel.com)
├── Org::Membership → Grand Copthorne Hotel, role: hq_manager (is_default: true)
├── Org::Membership → Orchard Hotel, role: hq_manager
└── Org::Membership → M Hotel, role: area_manager
└── OutletAssignments: [Lobby Bar, Banquet Hall]
One membership is marked as is_default: true — this is the company the user sees when they log in. They can switch companies from the UI without logging out.
This replaces the legacy SUPER_HQ_EXTERNAL role and the user_company pivot table. Multi-company access is no longer a special role — it is simply having more than one membership.
Invitation Flow
New users are added to a company through an invitation:
- An
hq_manager(orIdentities::Admin) invites a user by email - System creates an
Org::Invitewith the intended role (and optionally, outlet assignments) - Invitee receives the invitation email
- Invitee accepts — system creates
Identities::User(if new) andOrg::Membership - If outlet assignments were specified in the invitation,
Org::OutletAssignmentrecords are created
If the invitee already has an Identities::User (e.g., they already use Jod at another company), accepting the invitation creates an additional Org::Membership — they now have multi-company access.
Model Context
| Context | Details |
|---|---|
| Entity | Org::Membership (aggregate root — owns OutletAssignments) |
| Layer | Organisation Structure — Access Control |
| Upstream dependencies | Identities::User (the authenticated person), Org::Company (the company being accessed) |
| Downstream dependents | Org::OutletAssignment (outlet scoping), Org::Invite (invitation creates membership) |
State Machine
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | active | UC-1, UC-2, or UC-3: Membership created | User can access the company immediately. |
active | suspended | UC-6: Admin suspends membership | User cannot access the company. Data is preserved. |
suspended | active | UC-7: Admin reactivates membership | Access restored. |
active | revoked | UC-5: Membership removed | User loses access. Historical data preserved. |
suspended | revoked | UC-5: Membership removed while suspended | User loses access. |
Use Cases
| ID | Use Case | Trigger | Actor |
|---|---|---|---|
| UC-1 | Create membership during company sign-up | Employer creates a company (UC-1a of Org::Company) | Employer (new) |
| UC-2 | Create membership via invitation | Existing member invites a new user by email | hq_manager or Admin |
| UC-3 | Admin creates membership directly | Admin adds a user to a company during onboarding | Admin |
| UC-4 | Assign outlets to a membership | User needs scoped access to specific outlets | hq_manager or Admin |
| UC-5 | Remove a membership | User leaves the company or access is revoked | hq_manager or Admin |
| UC-6 | Suspend a membership | User temporarily cannot access the company | hq_manager or Admin |
| UC-7 | Reactivate a suspended membership | Suspension is lifted | hq_manager or Admin |
| UC-8 | Transfer ownership | Owner leaves the company or delegates ownership | Owner or Admin |
| UC-9 | Change a membership's role | User's responsibilities change within the company | hq_manager or Admin |
| UC-10 | Set default company | User with multiple memberships chooses their landing company | Employer |
| UC-11 | View members of a company | hq_manager reviews team members | hq_manager |
UC-1: Create membership during company sign-up
| Field | Details |
|---|---|
| Actor | Identities::User who does not yet have an Org::Membership |
| Trigger | Employer logs in and the system detects they have no Membership — shows the company creation form |
Preconditions:
Identities::Useralready exists (created earlier via the sign-up/registration endpoint)- The user has no existing
Org::Membership
System Behavior:
- User logs in. System detects they have no
Org::Membership. - System shows the company creation form (company name, registration number, address, etc.)
- User submits the form. System creates
Org::CompanyandOrg::Membershipin the same transaction:Org::Companywithparent_company_id = null,is_enabled = trueOrg::Membershipwithrole: hq_manager,is_owner: true,is_default: true
Business Rules:
- The first membership in a self-serve created company is always
hq_managerwithis_owner: true Org::CompanyandOrg::Membershipare created together in one transaction — you cannot create a Membership without a Company in this flow- The employer cannot create a Membership for an existing company through this flow — this is strictly for new company creation. To join an existing company, use the invitation flow (UC-2).
Postconditions:
Org::Companyexists withis_enabled = true- Membership exists with
role: hq_manager,is_owner: true,is_default: true - User can immediately access and manage the company
UC-2: Create membership via invitation
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | Existing member invites a new user by email |
Preconditions:
- Actor has
hq_managerrole in the company (or is admin) - No active membership exists for the invitee's email in this company
System Behavior:
- Actor enters the invitee's email address and selects a role
- Optionally, actor selects outlets to assign (for
area_manageroroutlet_manager) - System creates an
Org::Invitewith statuspending, the intended role, and outlet assignments - System sends invitation email to the invitee
- When invitee accepts:
- If the invitee has no
Identities::User: system creates one - System creates
Org::Membershipwith the specified role - If outlet assignments were specified: system creates
Org::OutletAssignmentrecords - If this is the user's first membership:
is_defaultis set totrue
- If the invitee has no
Business Rules:
- Only
hq_managermembers (or admins) can invite new users - The inviter must specify the role at invitation time
- A user cannot have two active memberships in the same company
- If the invitee already has a membership at another company, accepting creates a second membership — multi-company access
- Invitation has an expiry period (configured by system)
Postconditions:
Org::Inviteexists with statuspending(oracceptedafter acceptance)- On acceptance:
Org::Membershipexists with the specified role - On acceptance:
Org::OutletAssignmentrecords exist if outlets were specified
UC-3: Admin creates membership directly
| Field | Details |
|---|---|
| Actor | Identities::Admin |
| Trigger | Admin adds a user to a company during onboarding |
Preconditions:
Org::CompanyexistsIdentities::Userexists (or admin creates one)
System Behavior:
- Admin selects the company, user, and role
- System creates
Org::Membershipwith the specified role - If role requires outlet scoping: admin assigns outlets
Business Rules:
- Admin can create memberships with any role
- Admin can set
is_owner: trueduring onboarding - This path skips the invitation flow — membership is created immediately
Postconditions:
- Membership exists with the specified role
- User can access the company
UC-4: Assign outlets to a membership
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | User needs scoped access to specific outlets |
Preconditions:
- Membership exists with role
area_manageroroutlet_manager - Target outlets belong to the same company as the membership
System Behavior:
- Actor selects outlets to assign to the membership
- System creates
Org::OutletAssignmentrecords linking the membership to the outlets
Business Rules:
hq_managermemberships do not need outlet assignments — they have implicit access to all outletsarea_managercan be assigned to multiple outletsoutlet_managermust be assigned to exactly 1 outlet- Outlets must belong to the same
Org::Companyas the membership — no cross-company outlet assignments - Assignments can be added or removed at any time by an
hq_manageror admin
Postconditions:
Org::OutletAssignmentrecords exist for the membership- The user can now see and manage jobs at the assigned outlets
UC-5: Remove a membership
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | User leaves the company or access is revoked |
Preconditions:
- Membership exists and is
activeorsuspended - Target membership does not have
is_owner: true(unless actor is admin)
System Behavior:
- Actor removes the membership
- System sets membership status to
revoked - System soft-deletes all
Org::OutletAssignmentrecords for this membership (setsrevoked_at = now) - If this was the user's default company and they have other memberships: system sets another membership as default
Business Rules:
- An
hq_managercan remove any non-owner membership in their company - An
hq_managercannot remove the owner — only admin or the owner themselves can do this - Owner must transfer ownership (UC-8) before their membership can be removed
- Removal is a soft operation — both the membership record and its outlet assignments are preserved for audit
- Soft-deleted outlet assignments are excluded from active queries but remain queryable for historical audit ("which outlets did this user manage before they left?")
- If the user has no remaining active memberships, they lose all Org domain access (they may still have a
Talent::Profilefor worker features)
Postconditions:
- Membership status is
revoked - User can no longer access this company
- OutletAssignments soft-deleted (
revoked_atset)
UC-6: Suspend a membership
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | User temporarily cannot access the company (e.g., staff left or was reassigned) |
Preconditions:
- Membership exists and is
active - Target membership does not have
is_owner: true(unless actor is admin)
System Behavior:
- Actor suspends the membership
- System sets status to
suspended
Business Rules:
hq_managercan suspend any non-owner membership in their companyhq_managercannot suspend the owner — only admin can- Suspension preserves all data (outlet assignments, role)
- The user cannot access the company while suspended
- Suspension is reversible (UC-7)
Postconditions:
- Membership status is
suspended - User cannot access the company
UC-7: Reactivate a suspended membership
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | Suspension is lifted (e.g., staff returns or issue resolved) |
Preconditions:
- Membership exists and is
suspended
System Behavior:
- Actor reactivates the membership
- System sets status to
active
Business Rules:
hq_managercan reactivate any suspended membership in their company- All previous outlet assignments are restored (they were preserved during suspension)
Postconditions:
- Membership status is
active - User can access the company again with the same role and outlet assignments
UC-8: Transfer ownership
| Field | Details |
|---|---|
| Actor | Current owner, or Identities::Admin |
| Trigger | Owner leaves the company or delegates ownership |
Preconditions:
- Current owner has
is_owner: trueon their membership - Target user has an active
hq_managermembership in the same company
System Behavior:
- Actor selects the new owner from the list of
hq_managermembers - System sets
is_owner: falseon the current owner's membership - System sets
is_owner: trueon the new owner's membership
Business Rules:
- Ownership can only be transferred to an existing
hq_managerin the same company - There is exactly one owner per company at all times
- After transfer, the previous owner remains as a regular
hq_manager - Admin can force-transfer ownership without the current owner's consent
Postconditions:
- New owner has
is_owner: true - Previous owner has
is_owner: falsebut retainshq_managerrole
UC-9: Change a membership's role
| Field | Details |
|---|---|
| Actor | hq_manager of the company, or Identities::Admin |
| Trigger | User's responsibilities change within the company |
Preconditions:
- Membership exists and is
active - Target membership does not have
is_owner: true(owners must stayhq_manager)
System Behavior:
- Actor selects the new role for the membership
- System updates the role
- If changing from
hq_managerto a scoped role: actor must assign outlets - If changing from a scoped role to
hq_manager: system removes outlet assignments (no longer needed)
Business Rules:
- The owner's role cannot be changed — they must always be
hq_manager - When downgrading from
hq_managertoarea_manageroroutlet_manager, outlet assignments must be provided - When upgrading to
hq_manager, existing outlet assignments are removed (implicit full access)
Postconditions:
- Membership reflects the new role
- Outlet assignments updated accordingly
UC-10: Set default company
| Field | Details |
|---|---|
| Actor | The user themselves |
| Trigger | User with multiple memberships chooses their landing company |
Preconditions:
- User has more than one active
Org::Membership
System Behavior:
- User selects a company from their membership list
- System sets
is_default: trueon the selected membership - System sets
is_default: falseon all other memberships for this user
Business Rules:
- Exactly one membership per user has
is_default: true - If a user has only one membership, it is automatically the default
- The default company is the one shown when the user logs in
Postconditions:
- Selected membership has
is_default: true - Next login lands on this company's dashboard
UC-11: View members of a company
| Field | Details |
|---|---|
| Actor | hq_manager of the company |
| Trigger | hq_manager reviews team members |
Preconditions:
- Actor has
hq_managerrole in the company
System Behavior:
- System displays all active and suspended memberships for the company
- Each membership shows: user name, email, role, outlet assignments, owner status, last login
- Filterable by role and status
Business Rules:
- Only
hq_managercan view the full member list area_managerandoutlet_managercannot view other members- Revoked memberships are not shown (they are historical records)
Postconditions:
- Read-only operation — no data changes
Invariants
- Each
Org::Membershiplinks exactly oneIdentities::Userto exactly oneOrg::Company - A user cannot have two active memberships in the same company
- Each company has exactly one membership with
is_owner: true— this membership must have rolehq_manager - The owner's membership cannot be removed by another
hq_manager— only by admin or by ownership transfer - The owner's role cannot be changed — it must always be
hq_manager outlet_managermust have exactly 1Org::OutletAssignmentarea_managermust have at least 1Org::OutletAssignmenthq_managermust have zeroOrg::OutletAssignmentrecords — access to all outlets is implicit- Outlet assignments must reference outlets belonging to the same company as the membership
- Exactly one membership per user has
is_default: trueacross all their memberships - Memberships are never hard-deleted — revoked memberships are preserved for audit
- Role determines capabilities (fixed, not customizable per company). Outlet assignments determine scope.
Model Interactions
| Related Model | Relationship | Interaction |
|---|---|---|
Identities::User | Membership belongs_to User | The authenticated person. One user can have many memberships across different companies. |
Org::Company | Membership belongs_to Company | The company being accessed. One company has many memberships (its team members). |
Org::OutletAssignment | Membership has_many OutletAssignments | Scoping mechanism for area_manager and outlet_manager. Links membership to specific outlets. |
Org::Outlet | OutletAssignment belongs_to Outlet | The physical branch. Referenced by outlet assignments for scoped access. |
Org::Invite | Invitation creates Membership | Invitation carries the intended role and outlet assignments. Acceptance creates the membership. |
Billing::Account | (indirect, via Company) | hq_manager can view and manage the company's billing. Other roles have view-only access to credit history. |
Careers::Job | (indirect, via Company + Outlet) | Jobs are scoped by role: hq_manager sees all company jobs, scoped roles see only jobs at assigned outlets. |
Gig::TempJob | (indirect, via Company + Outlet) | Same scoping as Careers::Job. Gig jobs at assigned outlets only. |
Schema Gaps
| Gap | Impact | Suggested Resolution |
|---|---|---|
org_user_profiles table needs to be renamed or replaced | Current table does not have role, is_owner, is_default, or status columns | Resolved in DBML. org_memberships table defined with all required columns (role, is_owner, is_default, status). Migrate existing data from org_user_profiles. |
No org_outlet_assignments table exists | Cannot scope area_manager or outlet_manager to specific outlets | Resolved in DBML. org_outlet_assignments table defined with membership_id, outlet_id, revoked_at foreign keys and unique index. |
Org::Invite does not carry role or outlet assignments | Invitation cannot specify the intended role or outlet scope for the new member | Resolved in DBML. org_invites table has role column. org_invite_outlets join table defined for outlet pre-assignment. |