Partners — Domain Spec¶
A partner is a partners row plus a set of partner_memberships rows that grant individual users a scoped role within it. This page is the rules the rest of the product trusts about partner authority, scoping, and membership lifecycle.
Mental model¶
Three shapes are load-bearing:
- A partner is not a user.
Partneris its own entity with its ownid. Nothing in the consumer graph (friendship, chat, moments, presence) references a partner. The only thing that does reference a partner is the membership table and (in the future) partner-owned resources like events or listings. - Authority is membership-based. A user has authority over a partner only if a
PartnerMembershiprow exists for the (partner_id,user_id) pair. The user's globalaccount_typeandroleare irrelevant — astandarduser can be a partner owner, and anaccount_type = partneruser can have no memberships at all. - URL is the source of truth for scope. Every partner-bound action is at
/api/v1/partner/:partner_id/.... There is no "current partner" in the JWT or session — switching partners is navigation, not state.
Statuses and roles¶
Partner-level status:
| Status | Meaning | Set by |
|---|---|---|
active |
Operational | Default on Partner.Create |
suspended |
Members can still log in (account untouched), but partner-scoped endpoints return 403 | Admin action (TBD) |
Per-membership role:
| Role | May |
|---|---|
owner |
Manage billing (future), delete the partner, manage other owners/admins, plus everything below |
admin |
Manage staff, manage all partner resources, update the partner |
staff |
Manage partner resources only — no member management, no billing |
Sign-up / membership lifecycle¶
A partner is created by any authenticated user. The creator is automatically inserted as the first owner.
| Transition | Trigger |
|---|---|
(none) → membership |
POST /api/v1/partner/:partner_id/members by an existing admin/owner |
membership → (none) |
DELETE /api/v1/partner/:partner_id/members/:user_id by an admin/owner, OR self-removal |
role change |
PATCH /api/v1/partner/:partner_id/members/:user_id by an admin/owner |
The "≥1 owner per partner" invariant blocks two specific operations:
- Removing the last
ownerreturns409 Conflict(ErrLastOwner). - Demoting the last
ownertoadminorstaffreturns409 Conflict(ErrLastOwner).
To transfer ownership, promote someone else to owner first, then remove or demote the original owner.
Invariants¶
| Invariant | Why it matters |
|---|---|
| Slug is unique, lowercase alphanumeric + hyphen, 2–64 chars | URLs and display references are stable and predictable |
| One membership per (partner_id, user_id) | Enforced by composite unique index idx_partner_user |
≥1 owner per partner |
Enforced transactionally on remove / demote; blocks orphaning the tenant |
| Partner creation makes the caller owner | PartnerService.Create issues partner + membership atomically; rolls back partner if membership insert fails |
Account-type partner doesn't grant access to any partner |
Authority is membership-only; the flag only hides the account from consumer surfaces |
Authorization¶
Per-request authorization on partner-scoped routes goes through one of three middlewares in backend/internal/access:
| Middleware | Allowed roles | Used for |
|---|---|---|
RequirePartnerAnyMember |
owner / admin / staff | Read-only views (get partner, list members) |
RequirePartnerAdmin |
owner / admin | Updating partner, adding/removing/promoting members |
RequirePartnerOwner |
owner | Deleting the partner, future billing |
Each middleware reads :partner_id from the route and does one DB lookup against partner_memberships. There is no claim-side cache today; partner permission changes take effect on the next request.
Cross-domain interactions¶
| Other domain | Assumption |
|---|---|
| Identity | A user can belong to N partners; their account_type is independent of any membership. Standard accounts can be partner members. |
| Friends | Partner accounts (the rare case where a partner has its own login) are filtered out of friend search and friend suggestions. |
| Discovery | Partner accounts don't surface in user search. Partner-owned resources (events, moments, etc.) are not yet modelled — added per-feature when the first partner-owned thing ships. |
Privacy / visibility¶
| Field | Default | Notes |
|---|---|---|
| Partner name + slug | Public to authenticated users | Anyone can GET /api/v1/partner/:partner_id if they know the ID |
| Member list | Members only | Returned by GET /api/v1/partner/:partner_id/members to any member |
| Membership role | Members only | Same surface |
See also¶
- Narrative: Bible → Partners
- Backend: Backend → Partners service
- System view: Architecture → Authorization