The service arm of MOTIR-3500. Give docs/decisions/organization-tier.md §5 the count-1 arm it is missing, so an org invite into a one-workspace org lands the invitee somewhere they can work.
Today organizationsService.addMember (lib/services/organizationsService.ts:335) creates an OrganizationMembership and nothing else, at every workspace count. §5 says so deliberately ("an org-only member in zero workspaces is a valid state — e.g. a billing admin"), and tests/organizations-service.test.ts:191 asserts it. That reasoning holds only while the workspace tier offers a choice. §6 hides the tier entirely below two workspaces, so at count 1 the rule places the invitee in a tier that has no UI, no switcher, and no add-to-workspace action on the org roster.
A probe on origin/main @ d32892bd falsified the card as written (full evidence in this card's comments). Two corrections, and neither is optional:
1. The guarding READ needs a prerequisite. withOrgContext binds app.user_id + app.organization_id and no app.workspace_id, so workspaceRepository.listByOrganization(orgId, tx) returns the ACTOR's workspaces in the org, not the org's:
TRUE org workspace count = 2
SEEN under withOrgContext(founder) = 1
Uncorrected, the count-1 predicate fires in a two-workspace org whose inviting admin belongs to one of them — creating the very membership AC 3 forbids, while AC 3's own test still passes. The org-scoped read arm is now a separate prerequisite card and this card is blocked_by it. Do not re-derive a workaround: reading the count in a userless withOrgServiceWriteContext takes the guard out of the transaction it guards and misuses a helper scoped to the service-bearer billing path.
2. The WRITE needs the workspace GUC bound mid-transaction. membership_insert_active_or_bootstrap gates the INSERT on "workspaceId" = current_setting('app.workspace_id'), which the org context never binds:
INSERT under org ctx -> DriverAdapterError: new row violates row-level security policy for table "workspace_membership"
Call bindWorkspaceContext(tx, workspace.id) (lib/workspaces/context.ts:209) before the create. This is the exact downward mirror of workspacesService.addMember, which calls bindOrganizationContext(tx, …) for the upward join and documents the identical denial — read that function first and follow its shape, including its SECURITY constraint: the id must come from a TRUSTED resolution (here, the org's own workspace row), never from request input.
3. §5/§6 need a THIRD clause: whose count. The ADR's "count ≥ 2" reads as the ORG's, while the shipped gate ShellTierNav.tsx:103 counts listUserWorkspaces(session.user.id) filtered to the active org — the VIEWER's. They coincide only when the viewer belongs to every workspace in the org, which is the founder case. The amendment must say which predicate governs which clause: the org's count for §5's membership arm (this card — whether an invitee is auto-joined must not depend on the inviter's memberships) and the viewer's count for §6's disclosure (MOTIR-3502 — hiding a door the viewer cannot use is correct).
Inside addMember's existing withOrgContext transaction, after the assertOrgAdmin gate and the organizationMembershipRepository.create:
workspaceRepository.listByOrganization(orgId, tx) — correct only once the prerequisite arm has landed;bindWorkspaceContext(tx, thatWorkspace.id) and then create the WorkspaceMembership (role member) via workspaceMembershipRepository.create(..., tx) — same transaction, so an org member never exists without their workspace row;Idempotency mirrors what is already there: a unique violation on the workspace membership is swallowed (the row exists, the invariant holds), exactly as ensureOrgMembership does for the upward direction. The seat resync (enqueueScaledTrackerSeatSync) stays where it is, outside the tx.
addMemberByEmail needs no change — it delegates to addMember.
addMember into an org with exactly ONE workspace creates the target's WorkspaceMembership for that workspace with role member, and organizationsService.resolveWorkspaceAccess(target, ws) returns non-null.OrganizationMembership remains).addMember into an org with TWO workspaces creates NO WorkspaceMembership in either — and the fixture puts the ACTING ADMIN in exactly ONE of the two, which is the shape that fails against an actor-scoped count. tests/organizations-service.test.ts's existing "adding a user to the ORG creates NO workspace membership" case is re-pointed at a two-workspace org and still passes unchanged in substance.addMember twice for the same user is still idempotent-safe: the second call raises AlreadyOrgMemberError and leaves exactly one workspace membership.docs/decisions/organization-tier.md §5 AND §6 each state the count-1 arm, cross-reference each other, AND name which predicate each governs (org count for the membership arm, viewer count for the disclosure arm), so the clauses no longer contradict at the boundary.workspaceRepository method taking tx — no inline db.* in the service (CLAUDE.md § 4-layer).bindWorkspaceContext fails rather than silently passing under an owner connection.lib/services/organizationsService.ts:335 (addMember), :376 (addMemberByEmail), :468 (ensureOrgMembership — the upward analogue to mirror)lib/services/workspacesService.ts:573 (addMember) — the shipped mid-transaction GUC bind, and the comment recording the same RLS deniallib/workspaces/context.ts:209 (bindWorkspaceContext), lib/organizations/context.ts (withOrgContext, bindOrganizationContext)lib/repositories/workspaceMembershipRepository.ts:199, lib/repositories/workspaceRepository.ts:44prisma/migrations/20260810001000_tenant_root_insert_policies/migration.sql:110 — membership_insert_active_or_bootstrap, the policy that refuses the writedocs/decisions/organization-tier.md §5, §6app/(authed)/_components/ShellTierNav.tsx:103, app/(authed)/layout.tsx:108 — the shipped VIEWER-scoped predicate AC 5 must reconciletests/organizations-service.test.ts:191, tests/organizations-invite-by-email.test.ts, tests/rls/Repo: every criterion is discharged in motir-core.