Files
member-console/design/organization/model.md
Christian Galo 195cd348a9 Update design to use schemas per module and revert to using industry
aligned terminology for discounts and coupons and promo codes.
2026-04-01 03:08:35 -05:00

28 KiB
Raw Blame History

Organization & Access — Model Reference

Module: organization Schema: organization Source: data-model.md §Organization & Access

This file is a projection of the master data model reference. The technical content is identical to the corresponding section of data-model.md. When the master reference is updated, this file must be updated in the same change.


Organization & Access

Organization & Access answers: "What containers exist, and who can access them with what permissions?"


organizations

An organization is the top-level container for all resources, members, and billing relationships. Every person operates within the context of at least one organization.

There are no solo users in this model. Every person has a personal organization created automatically. This design choice means:

  • The data model is uniform whether someone is solo or on a team
  • Solo users can invite collaborators without migration
  • All resources, billing, and access control work the same way regardless of organization size
  • Organizations can be personal (one owner), team (multiple members), or enterprise (formal business)

Platform administration convention: The cooperative itself is represented as an organization in the model (e.g., slug = platform). Platform administrators are members of this organization with the platform_admin role. This reuses the existing org → members → roles mechanism for platform-level governance without requiring new schema entities.

Field Type Purpose
org_id UUID Primary key
name VARCHAR(255) Display name of the organization.
slug VARCHAR(100) URL-safe identifier. Globally unique. Used in URLs and API references.
org_type VARCHAR(20) Classification: personal (auto-created for individuals), team (collaborative), enterprise (formal business).
owner_person_id UUID FK → persons. For personal organizations, the person who owns it. Required when org_type = personal.
legal_name VARCHAR(255) Registered legal name of the business entity.
entity_type VARCHAR(50) Legal structure: llc, corporation, nonprofit, cooperative, sole_prop.
tax_id VARCHAR(100) Business tax identifier (EIN for US companies).
website VARCHAR(255) Organization's website.
settings JSONB Organization-level configuration and preferences.
status VARCHAR(20) State: active, suspended, deleted.
suspended_at TIMESTAMPTZ When most recently suspended.
suspended_by UUID FK → persons. Who suspended.
deleted_at TIMESTAMPTZ When deleted.
deleted_by UUID FK → persons. Who deleted.
created_at TIMESTAMPTZ When this organization was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • Personal organizations must have an owner_person_id. Enforced by: CHECK (org_type != 'personal' OR owner_person_id IS NOT NULL).
  • slug is globally unique to enable clean URLs.

Status lifecycle: Governed by the Soft-Delete and Terminal State Policy.

  • activesuspended (reversible): operational pause, suspended_at and suspended_by recorded.
  • activedeleted (terminal): soft-delete, deleted_at and deleted_by recorded.

Relationships:

  • organizations (0..1) → (1) persons: Personal organizations have an owner person. FK -> identity.persons
  • organizations (1) → (0..*) org_members: Organizations have members.
  • organizations (1) → (0..*) workspaces: Organizations contain workspaces.
  • organizations (1) → (0..*) billing_accounts: Organizations have billing accounts. FK -> billing.billing_accounts
  • organizations (1) → (0..*) resource_pools: Organizations have resource pools. FK -> entitlements.resource_pools
  • organizations (1) → (0..*) roles: Organizations may define custom roles.
  • organizations (1) → (0..*) service_accounts: Organizations have service accounts.
  • organizations (1) → (0..*) invitations: Organizations may have pending invitations.

org_members

An org_member represents the membership of a person in an organization with a specific role. This is the primary mechanism for organization-level access control.

Membership is distinct from ownership and distinct from scoped role assignments. Membership represents belonging — a person is part of this organization. Role assignments represent access — an actor can do specific things in a specific scope. A person might have a workspace-scoped role assignment in an organization they are not a member of (e.g., a contractor with access to one workspace).

Memberships are created upon invitation acceptance or direct administrative action — they are born active, never pending. Invitation state is tracked on the invitations entity.

Field Type Purpose
org_member_id UUID Primary key
org_id UUID FK → organizations. The organization.
person_id UUID FK → persons. The person who is a member.
role_id UUID FK → roles. The role this person has in this organization.
invitation_id UUID FK → invitations. The invitation that created this membership. NULL if created without an invitation (e.g., auto-created for personal org owner, or platform-level administrative action).
status VARCHAR(20) Membership state: active, suspended, removed.
suspended_at TIMESTAMPTZ When most recently suspended.
suspended_by UUID FK → persons. Who suspended.
removed_at TIMESTAMPTZ When removed.
removed_by UUID FK → persons. Who removed.
created_at TIMESTAMPTZ When this membership was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • A person can only be a member of an organization once. The combination of org_id and person_id is unique.

Status lifecycle: Governed by the Soft-Delete and Terminal State Policy.

  • activesuspended (reversible): suspended_at and suspended_by recorded.
  • activeremoved (terminal): removed_at and removed_by recorded.

Relationships:

  • org_members (0..*) → (1) organizations: Memberships belong to an organization.
  • org_members (0..*) → (1) persons: Memberships belong to a person. FK -> identity.persons
  • org_members (0..*) → (1) roles: Memberships have a role.
  • org_members (0..*) → (0..1) invitations: Memberships may trace their provenance to an invitation.

invitations

An invitation is a request for someone to join an organizational scope (organization or workspace) with a specific role. Invitations exist independently of both persons and org_members — the invitee may not yet have an account.

Invitee identity is progressive: an invitation begins addressed to an email and acquires richer identity references as its lifecycle progresses. This is a deliberate departure from the exclusive arc pattern used elsewhere in the model — the invitee identity evolves over the invitation's lifecycle rather than being fixed at creation.

Field Type Purpose
invitation_id UUID Primary key
invitee_email VARCHAR(255) Email address of the invitee. NULL if inviting an existing person directly.
invitee_person_id UUID FK → persons. NULL if email-only invitation. May be populated when an existing person is discovered for the email.
org_id UUID FK → organizations. If this is an organization-scoped invitation. NULL if workspace-scoped.
workspace_id UUID FK → workspaces. If this is a workspace-scoped invitation. NULL if org-scoped.
role_id UUID FK → roles. The role to be granted upon acceptance.
invited_by_person_id UUID FK → persons. Who created the invitation.
token_hash VARCHAR(255) Hashed invitation token. Plaintext sent via email/notification.
token_prefix VARCHAR(10) First characters for identification (e.g., mc_inv_d8f...).
message TEXT Optional personal message from the inviter.
sent_at TIMESTAMPTZ When the invitation was first sent.
last_sent_at TIMESTAMPTZ When most recently sent (initial or resend).
send_count INTEGER DEFAULT 1 Total times sent (initial + resends).
expires_at TIMESTAMPTZ When the invitation expires. Reset on resend.
accepted_at TIMESTAMPTZ When accepted.
resolved_person_id UUID FK → persons. The person who accepted. May be newly created (new signup) or pre-existing (existing user). Set upon acceptance.
resulting_member_id UUID FK → org_members. If acceptance created a membership.
resulting_assignment_id UUID FK → role_assignments. If acceptance created a role assignment.
declined_at TIMESTAMPTZ When declined.
revoked_at TIMESTAMPTZ When revoked.
revoked_by_person_id UUID FK → persons. Who revoked.
revocation_reason TEXT Why revoked.
status VARCHAR(20) pending, sent, accepted, declined, expired, revoked.
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Constraints (invitee — inclusive OR):

CHECK (invitee_email IS NOT NULL OR invitee_person_id IS NOT NULL)

At least one of invitee_email or invitee_person_id must be set. Both may coexist — this is progressive identity, not an exclusive arc. An invitation starts with the information available at creation time and acquires richer references as the lifecycle progresses.

Constraints (scope — exclusive arc):

CHECK (
  (org_id IS NOT NULL AND workspace_id IS NULL)
  OR (org_id IS NULL AND workspace_id IS NOT NULL)
)

Exactly one of org_id or workspace_id must be set. An invitation targets one organizational scope.

Uniqueness: Partial unique indexes enforce at most one active (non-terminal) invitation per (invitee, scope) combination.

Token format convention: mc_inv_<random>. Token is regenerated on resend (old token becomes invalid, new hash stored).

Status lifecycle:

  • pendingsent: Notification delivered.
  • sentsent: Resend (resets expires_at, increments send_count).
  • sentaccepted, declined, expired, revoked: Terminal states, all irrevocable.
  • pendingexpired, revoked: Terminal without delivery.

Relationships:

  • invitations (0..*) → (0..1) persons (via invitee_person_id): Invitations may be addressed to a known person. FK -> identity.persons
  • invitations (0..*) → (0..1) persons (via resolved_person_id): Accepted invitations record who accepted. FK -> identity.persons
  • invitations (0..*) → (1) persons (via invited_by_person_id): Invitations are created by a person. FK -> identity.persons
  • Exclusive arc — scope (org_id / workspace_id):
    • invitations (0..*) → (0..1) organizations: Org-scoped invitations target an organization.
    • invitations (0..*) → (0..1) workspaces: Workspace-scoped invitations target a workspace.
  • invitations (0..*) → (1) roles: Invitations specify a role to grant.
  • invitations (0..*) → (0..1) org_members (via resulting_member_id): Accepted org-scoped invitations create a membership.
  • invitations (0..*) → (0..1) role_assignments (via resulting_assignment_id): Accepted workspace-scoped invitations create an assignment.

workspaces

A workspace is a container for resources within an organization. Workspaces provide isolation, organization, and optionally different access controls within a single organization.

Field Type Purpose
workspace_id UUID Primary key
org_id UUID FK → organizations. The organization this workspace belongs to.
name VARCHAR(255) Display name of the workspace.
slug VARCHAR(100) URL-safe identifier. Unique within the organization.
description TEXT Explanation of what this workspace is for.
environment VARCHAR(20) Classification: development, staging, production. Informational.
settings JSONB Workspace-level configuration.
created_by_person_id UUID FK → persons. Who created this workspace.
status VARCHAR(20) State: active, archived, deleted.
archived_at TIMESTAMPTZ When archived.
archived_by UUID FK → persons. Who archived.
deleted_at TIMESTAMPTZ When deleted.
deleted_by UUID FK → persons. Who deleted.
created_at TIMESTAMPTZ When this workspace was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • slug is unique within an organization. The combination of org_id and slug is unique.

Status lifecycle: Governed by the Soft-Delete and Terminal State Policy.

  • activearchived (reversible): archived_at and archived_by recorded. Suspends access and prevents new resource creation but preserves all data.
  • activedeleted (terminal): deleted_at and deleted_by recorded. Signals permanent decommissioning.

Relationships:

  • workspaces (0..*) → (1) organizations: Workspaces belong to an organization.
  • workspaces (0..*) → (0..1) persons: Workspaces track who created them. FK -> identity.persons
  • workspaces (1) → (0..*) role_assignments: Workspaces may have scoped role assignments.
  • workspaces (1) → (1..*) pool_assignments: Workspaces are assigned to one or more resource pools. FK -> entitlements.pool_assignments
  • workspaces (1) → (0..*) usage_events: Workspaces record resource consumption. FK -> entitlements.usage_events
  • workspaces (1) → (0..*) invitations: Workspaces may have pending workspace-scoped invitations.

roles

A role defines a named set of permissions. Roles determine what actions an actor (person or service account) can perform within an organization, workspace, pool, or other scope.

Roles are either system-defined (built-in, applicable to all organizations) or organization-defined (custom roles for a specific organization). System roles provide consistent semantics across the platform. Custom roles allow organizations to tailor access control to their needs.

Permissions are flat and explicit. Each role states exactly what it grants as an array of permission strings. There is no implicit derivation, no inheritance. Given an actor's roles, you can enumerate their exact permissions without traversal.

Field Type Purpose
role_id UUID Primary key
org_id UUID FK → organizations. If set, this is a custom role for this organization. If NULL, this is a system role.
role_name VARCHAR(100) Machine-readable identifier. Unique within scope (system or per-org).
display_name VARCHAR(255) Human-readable name for UI.
description TEXT Explanation of what this role is for.
is_system BOOLEAN Whether this is a built-in system role. System roles cannot be deleted or modified.
permissions TEXT[] Array of permission strings. Format: resource:action (e.g., billing:manage, workspace:delete).
created_at TIMESTAMPTZ When this role was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • Custom roles may only contain permissions from the defined vocabulary. Application-level validation prevents invalid or stale permission strings.

System roles:

Role Purpose Key Differentiators
owner Full authority over the organization. Includes org:delete, org:transfer. Only role that can destroy or transfer the organization.
admin Full access except irreversible org-level actions. Everything except org:delete, org:transfer.
member Can work within workspaces and manage resources. No billing, pool, or org-structure access.
billing Financial access only. Can manage billing accounts, subscriptions, purchases, invoices. Cannot access workspaces or resources.
viewer Read-only access to all visible entities. Can see everything but modify nothing.
platform_admin Platform-level administrative access. Includes entitlement_rules:manage. Assigned within the platform organization only.

Relationships:

  • roles (0..*) → (0..1) organizations: Custom roles belong to an organization. System roles have no organization.
  • roles (1) → (0..*) org_members: Roles are assigned to organization members.
  • roles (1) → (0..*) role_assignments: Roles are assigned at scoped level.
  • roles (1) → (0..*) invitations: Roles are referenced by pending invitations.

role_assignments

A role_assignment grants a role to an actor (person or service account) for a specific scope. This enables finer-grained access control than organization-level membership.

Organization membership (via org_members) grants a baseline role to persons across the organization. Role assignments allow refinements and exceptions:

  • A person might be a viewer at org level but an admin for a specific workspace.
  • A contractor might have access to only one workspace, not the whole organization.
  • A service account might have access to exactly one workspace for CI/CD deployment.
  • Temporary access can be granted with an expiration date.

Service accounts have no org membership — their permissions come entirely from role assignments. This enforces minimum privilege by design.

Field Type Purpose
assignment_id UUID Primary key
person_id UUID FK → persons. If the assignee is a person. NULL if service account.
service_account_id UUID FK → service_accounts. If the assignee is a service account. NULL if person.
role_id UUID FK → roles. The role being granted.
scope_org_id UUID FK → organizations. If this assignment is organization-scoped. NULL otherwise.
scope_workspace_id UUID FK → workspaces. If this assignment is workspace-scoped. NULL otherwise.
scope_pool_id UUID FK → resource_pools. If this assignment is pool-scoped. NULL otherwise.
granted_by_person_id UUID FK → persons. Who granted this role. For audit trail.
granted_at TIMESTAMPTZ When this role was granted.
expires_at TIMESTAMPTZ When this role expires. NULL means no expiration.
revoked_at TIMESTAMPTZ When this role was revoked.
revoked_by_person_id UUID FK → persons. Who revoked.
status VARCHAR(20) State: active, revoked, expired.
created_at TIMESTAMPTZ When this assignment was created.

Constraints (exclusive arc — actor):

CHECK (
  (person_id IS NOT NULL AND service_account_id IS NULL)
  OR (person_id IS NULL AND service_account_id IS NOT NULL)
)

Exactly one of person_id or service_account_id must be set.

Constraints (exclusive arc — scope):

CHECK (
  (scope_org_id IS NOT NULL AND scope_workspace_id IS NULL AND scope_pool_id IS NULL)
  OR (scope_org_id IS NULL AND scope_workspace_id IS NOT NULL AND scope_pool_id IS NULL)
  OR (scope_org_id IS NULL AND scope_workspace_id IS NULL AND scope_pool_id IS NOT NULL)
)

Exactly one of scope_org_id, scope_workspace_id, or scope_pool_id must be set.

Uniqueness: The combination of actor (person_id or service_account_id), role_id, and scope (scope_org_id or scope_workspace_id or scope_pool_id) is unique. An actor cannot be assigned the same role to the same scope twice.

Relationships:

  • Exclusive arc — actor (person_id / service_account_id):
    • role_assignments (0..*) → (0..1) persons: Assignments may belong to a person. FK -> identity.persons
    • role_assignments (0..*) → (0..1) service_accounts: Assignments may belong to a service account.
  • role_assignments (0..*) → (1) roles: Assignments grant a role.
  • Exclusive arc — scope (scope_org_id / scope_workspace_id / scope_pool_id):
    • role_assignments (0..*) → (0..1) organizations: Assignments may be scoped to an organization.
    • role_assignments (0..*) → (0..1) workspaces: Assignments may be scoped to a workspace.
    • role_assignments (0..*) → (0..1) resource_pools: Assignments may be scoped to a pool. FK -> entitlements.resource_pools

service_accounts

A service_account is a non-human actor with its own identity and permissions. It represents an automated system or integration that interacts with the platform's API independently of any person.

Service accounts belong to organizations, not persons. They are created and managed by organization administrators. They have their own role assignments, independent of any person's permissions.

Service accounts are distinct from personal access tokens: a PAT acts as a person (inheriting their permissions); a service account acts as itself (with independently granted permissions). When a person leaves an organization, their PATs are revoked, but service accounts they created continue operating.

Service accounts are locally-managed by default, with optional Keycloak linkage. The oidc_subject and oidc_issuer fields allow linking to a Keycloak client's service account identity without requiring it. Creating a Keycloak client for every service account is unnecessarily heavy; local API keys are the primary authentication mechanism.

Field Type Purpose
service_account_id UUID Primary key
org_id UUID FK → organizations. The organization this service account belongs to.
name VARCHAR(255) Display name (e.g., "CI/CD Pipeline", "Nextcloud Connector", "Backup Service").
description TEXT What this service account does.
oidc_subject VARCHAR(255) If linked to a Keycloak service account: the OIDC subject identifier. NULL for locally-managed accounts.
oidc_issuer VARCHAR(255) If linked to Keycloak: the issuer. NULL for locally-managed accounts.
created_by_person_id UUID FK → persons. Who created this service account.
status VARCHAR(20) active, suspended, deleted.
suspended_at TIMESTAMPTZ When most recently suspended.
suspended_by UUID FK → persons. Who suspended.
deleted_at TIMESTAMPTZ When deleted.
deleted_by UUID FK → persons. Who deleted.
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Status lifecycle: Governed by the Soft-Delete and Terminal State Policy.

  • activesuspended (reversible): operational pause.
  • activedeleted (terminal): soft-delete.

Relationships:

  • service_accounts (0..*) → (1) organizations: Service accounts belong to an organization.
  • service_accounts (0..*) → (0..1) persons: Service accounts track who created them. FK -> identity.persons
  • service_accounts (1) → (0..*) service_account_keys: Service accounts have API keys.
  • service_accounts (1) → (0..*) role_assignments: Service accounts receive scoped role assignments.

service_account_keys

A service_account_key is a credential for a service account. Service accounts may have multiple keys to support rotation: a new key is created before the old one is retired, ensuring uninterrupted access during the transition.

Field Type Purpose
key_id UUID Primary key
service_account_id UUID FK → service_accounts.
name VARCHAR(255) Human-readable name (e.g., "Production key", "Rotation 2026-02").
key_hash VARCHAR(255) Hashed key value. Plaintext shown once at creation.
key_prefix VARCHAR(10) First few characters for identification (e.g., "mc_sak_b7d...").
expires_at TIMESTAMPTZ When this key expires. NULL for no expiration.
last_used_at TIMESTAMPTZ When this key was last used.
last_used_ip INET IP address of last use.
revoked_at TIMESTAMPTZ When revoked.
revoked_by_person_id UUID FK → persons. Who revoked it.
status VARCHAR(20) active, expired, revoked.
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Token format convention: mc_sak_<random> (Member Console, Service Account Key). Distinct from PATs for immediate identification.

Status lifecycle:

  • activeexpired (passive): key reaches expires_at.
  • activerevoked (terminal): explicit revocation with actor attribution.

Relationships:

  • service_account_keys (0..*) → (1) service_accounts: Keys belong to a service account.
  • service_account_keys (0..*) → (0..1) persons (via revoked_by_person_id): Revocation actor. FK -> identity.persons

Permission System

Permission String Format

Permissions follow the format resource:action. Resources correspond to entity types and management surfaces. Actions are a small, consistent set.

Actions:

Action Meaning
view Read/list the resource
create Create new instances
edit Modify existing instances
delete Remove instances
manage Full CRUD plus resource-specific administrative actions

Permission Vocabulary

Organization: org:view, org:edit, org:delete, org:transfer, org.members:view, org.members:manage, org.service_accounts:view, org.service_accounts:manage

Workspaces: workspace:view, workspace:create, workspace:edit, workspace:delete, workspace.resources:view, workspace.resources:manage

Resource Pools: pool:view, pool:create, pool:edit, pool:delete, pool.assignments:view, pool.assignments:manage, pool.ondemand:view, pool.ondemand:manage

Billing: billing:view, billing:manage, billing.subscriptions:view, billing.subscriptions:manage, billing.purchases:view, billing.purchases:create, billing.invoices:view

Grants & Entitlement Rules: grants:view, grants:manage, entitlement_rules:view, entitlement_rules:manage

Administration: roles:view, roles:manage, audit:view, tokens:manage

System Role Permission Sets

owner: org:view, org:edit, org:delete, org:transfer, org.members:view, org.members:manage, org.service_accounts:view, org.service_accounts:manage, workspace:view, workspace:create, workspace:edit, workspace:delete, workspace.resources:view, workspace.resources:manage, pool:view, pool:create, pool:edit, pool:delete, pool.assignments:view, pool.assignments:manage, pool.ondemand:view, pool.ondemand:manage, billing:view, billing:manage, billing.subscriptions:view, billing.subscriptions:manage, billing.purchases:view, billing.purchases:create, billing.invoices:view, grants:view, grants:manage, entitlement_rules:view, roles:view, roles:manage, audit:view

admin: Same as owner minus org:delete, org:transfer.

member: org:view, org.members:view, workspace:view, workspace.resources:view, workspace.resources:manage, pool:view, pool.assignments:view, billing.invoices:view

billing: org:view, billing:view, billing:manage, billing.subscriptions:view, billing.subscriptions:manage, billing.purchases:view, billing.purchases:create, billing.invoices:view, pool:view, pool.ondemand:view

viewer: org:view, org.members:view, workspace:view, workspace.resources:view, pool:view, pool.assignments:view, pool.ondemand:view, billing:view, billing.subscriptions:view, billing.purchases:view, billing.invoices:view, audit:view

platform_admin: All admin permissions plus entitlement_rules:view, entitlement_rules:manage. Assigned within the platform organization only.

Permission Resolution Algorithm

Permissions are additive (union) and deny-by-default. An actor can do nothing unless a permission explicitly allows it. There are no deny rules.

For persons:

effective_permissions(person, context) =
  permissions_from_org_membership(person, context.org_id)
   permissions_from_scoped_assignments(person, context.scope)

If a person has workspace.resources:view from their org membership and workspace.resources:manage from a workspace-scoped assignment, they can manage resources in that workspace.

For service accounts:

effective_permissions(service_account, context) =
  permissions_from_scoped_assignments(service_account, context.scope)

Service accounts have no org membership baseline. Their permissions come entirely from role assignments.

For personal access tokens:

effective_permissions(token, context) =
  effective_permissions(token.person, context) ∩ token.scopes

When scopes is NULL, the PAT has the person's full permissions. When set, the intersection narrows the token's capabilities.