Files
member-console/openspec/specs/organization/spec.md

6.3 KiB

organization

Purpose

Defines the organization model: organizations, members, workspaces, roles, and scoped role assignments.

Requirements

Requirement: Organizations table

The system SHALL maintain an organizations table with fields: org_id (UUIDv7 primary key, DEFAULT uuidv7()), name (VARCHAR(255), NOT NULL), slug (VARCHAR(100), UNIQUE, NOT NULL), org_type (VARCHAR(20), NOT NULL, FK → organization.org_types(org_type)), owner_person_id (UUID, FK → persons, NOT NULL), status (VARCHAR(20), NOT NULL, DEFAULT 'active'), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ).

Scenario: Organization slug uniqueness

  • WHEN an attempt is made to create an organization with a slug that already exists
  • THEN the database SHALL reject the insert with a unique constraint violation

Scenario: Organization has an owner

  • WHEN an organization is created
  • THEN it SHALL reference a persons record as owner_person_id

Scenario: Organization type referential integrity

  • WHEN an attempt is made to create an organization with an org_type that does not exist in org_types
  • THEN the database SHALL reject the insert with a foreign key constraint violation

Requirement: Organization members

The system SHALL maintain an org_members table with fields: org_member_id (UUIDv7 primary key), org_id (UUID, FK → organizations, NOT NULL), person_id (UUID, FK → persons, NOT NULL), role_id (UUID, FK → roles, NOT NULL), status (VARCHAR(20), NOT NULL, DEFAULT 'active'), joined_at (TIMESTAMPTZ, NOT NULL, DEFAULT NOW()), removed_at (TIMESTAMPTZ), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ). The combination of org_id and person_id SHALL be unique (a person can be a member of an org only once).

Scenario: Person joins organization

  • WHEN an org_members record is created linking a person to an organization with a role
  • THEN the person SHALL be considered a member of that organization with the permissions granted by the assigned role

Scenario: Duplicate membership prevented

  • WHEN an attempt is made to add a person to an organization they already belong to
  • THEN the database SHALL reject the insert with a unique constraint violation on (org_id, person_id)

Requirement: Workspaces

The system SHALL maintain a workspaces table with fields: workspace_id (UUIDv7 primary key), org_id (UUID, FK → organizations, NOT NULL), name (VARCHAR(255), NOT NULL), slug (VARCHAR(100), NOT NULL), description (TEXT), status (VARCHAR(20), NOT NULL, DEFAULT 'active'), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ). The combination of org_id and slug SHALL be unique (workspace slugs are unique within an organization).

Scenario: Workspace belongs to organization

  • WHEN a workspace is created
  • THEN it SHALL reference exactly one organization via org_id

Scenario: Workspace slug unique within org

  • WHEN an attempt is made to create a workspace with a slug that already exists within the same organization
  • THEN the database SHALL reject the insert with a unique constraint violation on (org_id, slug)

Requirement: Roles with flat permission model

The system SHALL maintain a roles table with fields: role_id (UUIDv7 primary key), org_id (UUID, FK → organizations, nullable), role_name (VARCHAR(100), NOT NULL), display_name (VARCHAR(255), NOT NULL), description (TEXT), is_system (BOOLEAN, NOT NULL, DEFAULT FALSE), permissions (TEXT[], NOT NULL), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ). System roles (is_system = TRUE) have org_id = NULL. Custom roles have org_id set.

Scenario: System roles are seeded

  • WHEN the organization module's migration runs
  • THEN the following system roles SHALL exist: owner, admin, member, billing, viewer, platform_admin
  • AND each role SHALL have the permission arrays defined in the design's role specification
  • AND all system roles SHALL have is_system = TRUE and org_id = NULL

Scenario: Permission format

  • WHEN a role's permissions are queried
  • THEN each permission SHALL be a string in resource:action format (e.g., billing:manage, workspace:delete)

Requirement: Scoped role assignments

The system SHALL maintain a role_assignments table with fields: assignment_id (UUIDv7 primary key), role_id (UUID, FK → roles, NOT NULL), person_id (UUID, FK → persons, NOT NULL), org_id (UUID, FK → organizations, NOT NULL), scope_type (VARCHAR(20), NOT NULL — values: organization, workspace, pool), scope_id (UUID, NOT NULL), created_at (TIMESTAMPTZ), updated_at (TIMESTAMPTZ). The combination of role_id, person_id, org_id, scope_type, and scope_id SHALL be unique.

Scenario: Organization-scoped assignment

  • WHEN a role assignment is created with scope_type = 'organization' and scope_id equal to an org_id
  • THEN the person SHALL have that role's permissions for the entire organization

Scenario: Workspace-scoped assignment

  • WHEN a role assignment is created with scope_type = 'workspace' and scope_id equal to a workspace_id
  • THEN the person SHALL have that role's permissions for that specific workspace only

Requirement: Organization module owns its schema

The organization module SHALL own its database schema via migrations in internal/organization/migrations/. The organization module's sqlc configuration SHALL read both identity and organization migrations to resolve cross-module FK references. It SHALL generate into the internal/organization/ Go package.

Scenario: Organization migration creates tables

  • WHEN the organization module's migration runs (after identity migration)
  • THEN the organizations, org_members, workspaces, roles, and role_assignments tables SHALL exist with all specified columns, constraints, and indexes

Requirement: Updated at triggers

The system SHALL automatically update the updated_at timestamp on organizations, org_members, workspaces, roles, and role_assignments rows whenever they are modified.

Scenario: Organization record updated

  • WHEN any field on an organizations record is modified
  • THEN the updated_at field SHALL be set to the current timestamp via a database trigger