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-388

6.10.3 Schema — `Organization` + `OrganizationMembership` + `Workspace.organizationId` + migration + backfill

Done
Description

Estimate: 65m · Depends on: 6.10.2

Implement the schema for the org tier decided in 6.10.2, with a migration that BACKFILLS every existing workspace into a default org so no legacy data is orphaned. This is the data foundation the gating (6.10.4), the UI (6.10.5), and the seed (6.10.6) build on.

Schema (prisma/schema.prisma):

  • Organization — the root tenant: { id, name, slug, createdAt, updatedAt, ... } with slug unique. The NEW top tier (do NOT touch Better-Auth’s Account).
  • OrganizationMembership — the membership join (mirrors WorkspaceMembership): { id, organizationId, userId, role (OrganizationRole owner|admin|member), createdAt }, unique on (organizationId, userId). Modelled as @relation to both Organization and User (back-relations on each).
  • enum OrganizationRoleowner | admin | member (the org-scoped role from 6.10.2, distinct from the 6.4 MemberRole).
  • Workspace.organizationId — a NON-nullable FK to Organization (after backfill), modelled as a Prisma @relation (Workspace.organizationOrganization.workspaces) with the matching onDelete/onUpdateNEVER a raw-SQL-only FK left as a bare scalar (the CLAUDE.md FK-as-@relation rule — a split would put the schema graph + migrate DB in permanent drift).

The migration + backfill (the load-bearing part). A single migration that: (1) creates Organization + OrganizationMembership + the enum; (2) adds Workspace.organizationId NULLABLE first; (3) BACKFILLS — for EACH existing workspace, create one default Organization (named/slugged from the workspace), point the workspace at it, and create an OrganizationMembership(owner) for the workspace’s owner/first-admin (so legacy data has an org owner); (4) makes Workspace.organizationId NON-nullable once every row is set. The backfill is idempotent / re-runnable-safe. Because the project uses prisma migrate + the shared dev DB, hand-author the data-backfill SQL in the migration (mirror the prodect-shared-db migrate pattern) so it is deterministic.

Repositories (single-op each — 4-layer). organizationRepository (find / create / update by id+slug) and organizationMembershipRepository (find-by-org+user, create [tx], list-by-org, delete [tx], update-role [tx]) — writes REQUIRE tx per CLAUDE.md. NO business logic here (that is 6.10.4’s service).

Acceptance criteria

  • prisma/schema.prisma gains Organization, OrganizationMembership, enum OrganizationRole, and Workspace.organizationId — every FK modelled as a Prisma @relation on BOTH sides (no raw-SQL-only FK); prisma migrate dev reports "No difference detected" after the migration (no spurious DROP CONSTRAINT — the FK-drift rule).
  • The migration backfills exactly ONE default org per pre-existing workspace, points each workspace at it, and creates an owner OrganizationMembership for each; after it, NO workspace has a null organizationId and the column is NON-nullable.
  • The backfill is idempotent (re-running it creates no duplicate orgs/memberships).
  • organizationRepository + organizationMembershipRepository exist as single-op repos; write methods require tx; Better-Auth’s Account model is untouched.

Context refs

  • 6.10.2 — the model + backfill decision this implements.
  • motir-core/prisma/schema.prismaWorkspace, WorkspaceMembership, MemberRole, User, and the existing Account (Better-Auth — do NOT reuse); the patterns OrganizationMembership mirrors.
  • motir-core/lib/repositories/workspaceMembershipRepository.ts — the single-op + required-tx repo pattern to mirror.
  • motir-core/CLAUDE.md § FK-as-@relation (the FK-drift rule) + § 4-layer (repository layer).