Files
member-console/openspec/specs/organization/spec.md
T
cgalo5758 8e3c68c6be Make UI surfaces honestly reflect system state
- Add deployment-name branding to titles, mastheads, and OG tags
- Share one grant delivery-state query with lineage across grants
  surfaces
- Show pool status/usage, org owners, and config readiness
- Make billing views projection-aware with recency and sync vocabulary
- Guard FedWiki creation without domains and render route-aware 404s
2026-08-23 01:45:52 -05:00

8.4 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

Requirement: Operator presentation of organizations is honest about what the system knows

Operator surfaces presenting organizations, memberships, and persons SHALL NOT render state the console does not master or evaluate as if it were live state:

  • The organizations list SHALL NOT render organizations.status as a colored state badge while nothing in the system reads that column; the column is omitted or rendered as neutral text until a real lifecycle consumes it.
  • Each organization SHALL display its owner (the guaranteed owner membership) on the list and the detail surface, so an organization cannot read as unowned.
  • The reserved System organization SHALL carry a visible synthetic marker distinguishing it from member organizations, and its row SHALL NOT invite ordinary member-org actions as if it were one.
  • Membership role badges SHALL NOT imply console authorization: where a database role (e.g. owner) is displayed, the surface SHALL make clear it is an organizational fact, not a permission grant (authorization derives from IdP roles at login).
  • Org types SHALL render once, by display name; concatenations that double display name and slug (e.g. "Personal personal") are retired.
  • Person surfaces SHALL NOT badge a person as active or otherwise imply account-enablement knowledge that lives in the identity provider.

Scenario: Organization rows show an owner

  • WHEN an operator views the organizations list
  • THEN each row identifies the organization's owner person

Scenario: Unread status is not badged

  • WHEN an operator views the organizations list
  • THEN no green state badge renders from the unread status column

Scenario: System organization is marked synthetic

  • WHEN an operator views a listing that includes the reserved System organization
  • THEN the row carries a visible marker naming it as the synthetic system tenant

Scenario: Org type renders once by display name

  • WHEN an operator views any surface rendering an organization's type
  • THEN the type renders as its display name a single time, with no appended slug duplicate