Skip to content

feat(orgs): delete, leave, suspend, My Organizations, and org LinkedIn pages - G25 #54

Description

@Adron

Summary

The client can create and read organizations and manage members' roles, but cannot delete an org, leave one, suspend a member, or see My Organizations as its own surface. The whole organization-LinkedIn feature — a real, documented product capability — is absent.

Route gaps (all live, Bearer-reachable)

Route What it unlocks
DELETE /api/organizations/{id} Delete an organization (owner only)
GET /api/user/organizations My Organizations — orgs you belong to, with your role
POST /api/user/organizations (probe: likely the join path — confirm before use)
GET /api/organizations/{id}/linkedin/status Org LinkedIn credential status
POST /api/organizations/{id}/linkedin/sync-pages Pull the org's LinkedIn company pages
PUT /api/organizations/{id}/linkedin/assignments Assign company pages to members
DELETE /api/organizations/{id}/linkedin/credential Disconnect the org credential

Live shape for GET /api/user/organizations (2026-09-07) — richer than the browse list, carrying the caller's role and membership:

{"organizations":[{"id":"fb29220f-…","name":"Bikey Life","slug":"bikey-life",
                   "description":"…","avatar":null,"isPublic":true,"isSystem":false,
                   "settings":null,"createdAt":"…","updatedAt":"…","deletedAt":null,
                   "role":"member","joinedAt":"…","userRole":"member","memberCount":3}]}

Note isSystem — that is how "The Public" is flagged, and it must not offer a Leave action.

Behaviour gaps (/help/organizations)

  • Suspend a member. "Change an existing member's role, suspend their access, or remove them using the controls in the members table." macOS OrgMembersViewModel has changeRole, addMember, removeMember, lookupUser, addMemberByHandle — no suspend.
  • Leave an organization. "Use the Leave button next to an organization in the Organizations list or in your My Organizations view."
  • Last-owner protection. "The last remaining owner cannot be demoted or removed" and cannot leave until another owner exists. Enforce client-side so the user gets an immediate, specific message rather than a server rejection.
  • "The Public" is a system org everyone auto-joins and nobody can leave.
  • Creating an org is subscriber-gated (server returns 403 otherwise); joining is free. Covered by the entitlement issue — consume the gate here, do not re-implement it.

Org LinkedIn, in the web's words

An owner or admin connects the org's LinkedIn credential (requesting LinkedIn's organization-admin scope). The company pages LinkedIn returns are stored on the organization. Owners and admins assign specific company pages to specific members. When an assigned member cross-posts to LinkedIn, it can publish to their assigned company page using the shared org credential. Members without an assignment fall back to their personal LinkedIn identity.

For a member who has an assignment, the assigned org page also becomes their default LinkedIn destination … If an owner or admin later disconnects the org's credential, every assigned member falls back to their personal LinkedIn.

That last paragraph is the part with teeth: an org assignment changes where a member's ordinary LinkedIn cross-post goes. The composer's "Posting as …" readiness line (shipped as G11a) must reflect the assigned page, or a member will believe they are posting to their own profile while publishing to a company page.

⚠️ Re-confirm orgScopesEnabled on this tenant before building — the org LinkedIn routes were previously recorded as not deployed and only appeared in the 2026-09-05 spec.

Division of labor

Kit

  • Organizations.delete(id:), User.organizations(), and the four org-LinkedIn routes + DTOs
  • Confirm what POST /api/user/organizations does before building against it
  • Contract tests

Domain

  • OrganizationsService: delete, leave, suspend-member, my-organizations
  • Encode last-owner protection and the isSystem no-leave rule as client-side preconditions with their own error cases
  • Org LinkedIn: status, sync-pages, assignments, disconnect
  • Feed the member's effective LinkedIn destination into the composer's existing posting-target projection

App

  • A My Organizations surface showing role, joined date, and member count, with per-row Leave
  • Owner-only Delete, with a confirmation that names the org and states it is not reversible
  • Suspend / unsuspend in the members table
  • An org LinkedIn section for owners and admins: connect, sync pages, assign pages to members, disconnect (disconnect is destructive for every assigned member — say so)
  • Composer: when the signed-in member has an org page assignment, "Posting as …" names the company page

Tests

  • happy — leave a non-system org; delete an owned org; suspend and restore a member; assign a page
  • invalid — last owner tries to leave / be demoted; anyone tries to leave "The Public"
  • upstream-failure — sync-pages fails → assignments UI stays usable with the stale page list, error surfaced
  • boundary — an org with exactly one owner and one member; a member with and without a page assignment

Acceptance criteria

  • Every organization action the web offers is available on the Mac, with last-owner and system-org rules enforced before the network call.
  • A member with an org page assignment can tell where their LinkedIn cross-post is going.
  • Full E2E gate green.

Notes

work-consolidation.md tracks this as G25 (formerly G11b, no longer upstream-blocked). Size M. The personal-scope LinkedIn work (PUT /api/linkedin/posting-targets, POST /api/linkedin/sync-pages) lives in the Integrations issue — coordinate so the LinkedIn domain model is written once.

Activity

  1. added
    enhancementNew feature or request
    parityWeb-parity gap with the InterlinedList web app
    on Sep 7, 2026
  2. Adron commented on Sep 13, 2026

    @Adron
    MemberAuthor

    Shipped in PR #67, merged to dev as dc0d54a — org delete, leave, suspend/restore, My Organizations with join, and the four org-LinkedIn routes, with last-owner protection and the "nobody leaves The Public" rule enforced client-side.

    Two silent decode defects were fixed on the way: OrganizationMemberDTO required userId where the live rows key by id/joinedAt (so the roster could never render), and LinkedInTargetDTO.kind had the wrong taxonomy, which made the composer's "Posting as …" line actively wrong for org-page destinations.

    Closed as part of the 2026-09-13 tracker reconciliation. These stayed open only because PRs target dev rather than the default branch, so GitHub's Closes #NN keyword never fired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions