MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-389

6.10.4 Org-scoped services + access gating — org membership gates workspace access; org owner/admin extends 6.4 roles

Done
Description

Estimate: 70m · Depends on: 6.10.3

Build the org service layer + the ACCESS GATING that makes the org tier real: org membership gates workspace access, and the org owner/admin role extends (sits above) the 6.4 workspace role model. This is the business logic the org admin API/UI (6.10.5) and the platform stories later sit on.

organizationsService (business logic + transactions — 4-layer):

  • Org CRUD (create/rename/update an org), membership management (invite/add a user to the org with an OrganizationRole, remove, change role), and "list members ACROSS the org’s workspaces" (the cross-workspace roster — PAGINATED, never load-all, the at-scale rule). Each write-flow is ONE prisma.$transaction; returns DTOs via lib/mappers/* (never raw Prisma); throws typed errors from lib/organizations/errors.ts the route maps to HTTP.
  • Membership DIRECTION (6.10.2 §5, asymmetric — enforce in the service, NOT scattered): adding a user to the ORG creates ONLY an OrganizationMembership — NO workspace membership (org-only members in zero workspaces are valid; a plain org member reaches only workspaces they’re explicitly added to). Conversely the add-to-WORKSPACE flow (extending the existing WorkspaceMembership create) MUST auto-create the user’s OrganizationMembership (role member) in that workspace’s org if absent — the UPWARD invariant (you cannot be in a workspace without being in its org), in the SAME transaction. Removing from the org cascades loss of workspace access (gate); removing from a workspace leaves the org membership intact.
  • Resolving the ACTIVE org for a session (for the switcher) + listing the orgs a user belongs to.
  • Auto-provision on signup (the progressive-disclosure principle, 6.10.2 §6). A provisionForNewUser-style flow that creates an org + a default workspace + the owner memberships for a brand-new account, in ONE transaction, wired into the existing signup/onboarding path — so every account is an org of one (OPC) from day one and there is never a tier-less user. Mirror the shape of the 6.10.3 backfill (which does the same for pre-existing workspaces); the org name defaults from the user/company and is renameable.
  • (Make the create-workspace path org-aware so a new workspace is created under the active org with the creator as a member. The copy-on-create deep-copy of the source workspace’s config — the "looks-inherited" behaviour, 6.10.2 §6e — is its OWN subtask 6.10.9, which extends this flow; do not inline it here.)

The access gate (the load-bearing change). Extend the existing workspace access check so that reaching a workspace requires the session user to be a member of the workspace’s ORG (org membership gates workspace access — 6.10.2). Compose the roles per 6.10.2’s precedence: an org OWNER/ADMIN is granted admin-equivalent access to EVERY workspace under the org (extending the 6.4 MemberRole); an org MEMBER falls back to their per-workspace MemberRole. Preserve the 404-not-403 cross-tenant posture (a non-member of the org sees the workspace as not-found, not forbidden — the standing guard).

Where it threads. The gate is a single authorization helper the existing workspace-scoped services/route guards call (do NOT scatter org checks across N routes); the 6.4 permission helper is extended, not duplicated. Reads that guard a write take tx + SELECT FOR UPDATE where a concurrent membership change could race (the lock-before-read-derived-update rule).

Acceptance criteria

  • organizationsService owns org CRUD + membership management + the cross-workspace member listing (paginated), each write-flow in ONE transaction, returning DTOs (never raw Prisma), throwing typed errors; routes (added in 6.10.5) call exactly one service method.
  • The access gate denies a user who is a member of a workspace but NOT of its org (org membership gates workspace access), and a non-org-member gets 404-not-403 cross-tenant.
  • An org owner/admin is granted admin-equivalent access to every workspace under the org (the role composes ABOVE the 6.4 MemberRole); an org member falls back to their per-workspace role.
  • The gate is a single shared helper (not scattered per route); the 6.4 permission check is extended, not duplicated.
  • 4-layer respected throughout (service owns transactions; repos are single-op with required tx on writes).

Context refs

  • 6.10.2 — the gating + role-precedence decision this implements.
  • 6.10.3 — the organizationRepository / organizationMembershipRepository + the schema this orchestrates.
  • motir-core/lib/services/ — the workspace + membership services + the 6.4 permission/role helper the gate EXTENDS (mirror its shape).
  • motir-core/CLAUDE.md § 4-layer + the lock-before-read-derived-update rule + the 404-not-403 cross-tenant guard.