Skip to main content

SPEC 012 — Team Members and Roles

FieldValue
StatusDRAFT
PriorityP1 — Core Product
Backendequa-server/modules/organizations/, equa-server/modules/common/
APIequa-server/modules/api/src/endpoints/organization-endpoints.ts
Frontendequa-web/src/modules/team-members/, equa-web/src/modules/roles/

1. Feature Purpose

Team Members and Roles is the authorization backbone of the Equa platform. Every organization has members (people associated with the org), roles (named permission groups), and a permission system that gates access to cap table, documents, billing, and all other features. This module handles inviting new members, managing their profiles, creating custom roles, assigning permissions, and enforcing access control across every API endpoint.

2. Current State (Verified)

2.1 Backend

ComponentPath
Persistence entitiesequa-server/modules/persistence/src/schema.ts
Member/role readingequa-server/modules/persistence/src/organizations/reading.ts
Member/role writingequa-server/modules/persistence/src/organizations/writing.ts
Common readingequa-server/modules/persistence/src/common/reading.ts
Common writingequa-server/modules/persistence/src/common/writing.ts
Invitation logicequa-server/modules/organizations/src/invitations.ts
Permission enforcementequa-server/modules/api/src/site/authorization.ts
Permission enumequa-server/modules/common/src/types.ts
API endpointsequa-server/modules/api/src/endpoints/organization-endpoints.ts

2.2 Frontend

ComponentPath
Team members moduleequa-web/src/modules/team-members/
Roles moduleequa-web/src/modules/roles/
Member form typesequa-web/src/modules/team-members/types.ts
Roles utilityequa-web/src/modules/roles/utility.ts
Roles service layerequa-web/src/service/services/roles/
Organizations serviceequa-web/src/service/services/organizations/
Shared permissionsequa-web/src/shared/components/permissions.tsx

3. Data Model

Members

ColumnTypeConstraintsDescription
iduuidPKMember identifier
titlevarcharnullableJob title
emailcitextnullableCase-insensitive email
fullNamevarcharDisplay name
dateOfBirthdatenullable
addressvarcharnullablePostal address
phonevarcharnullablePhone number
organizationuuidFK → OrganizationsOwning organization
useruuidnullable, FK → UsersLinked Equa user account (null if not yet registered)
isIndividualbooleandefault: trueIndividual vs entity/company member
Source: schema.ts lines 892–922

Roles

ColumnTypeConstraintsDescription
iduuidPKRole identifier
namevarcharRole display name
descriptionvarchardefault: ”Role description
owneruuidnullableOrganization or user that owns the role
isSharedbooleandefault: falseWhether role is shared across organizations
Source: schema.ts lines 1305–1321

MembersRoles (Join Table)

ColumnTypeConstraints
memberuuidPK, FK → Members
roleuuidPK, FK → Roles
Source: schema.ts lines 1296–1303

OrganizationsRoles (Join Table)

ColumnTypeConstraints
organizationuuidPK, FK → Organizations
roleuuidPK, FK → Roles
Source: schema.ts lines 1323–1330

Permissions

ColumnTypeConstraints
iduuidPK
namevarcharPermission identifier string
Source: schema.ts lines 1332–1339

PermissionsRoles (Join Table)

ColumnTypeConstraints
permissionuuidPK, FK → Permissions
roleuuidPK, FK → Roles
Source: schema.ts lines 1341–1348

GlobalRolesUsers

ColumnTypeConstraintsDescription
useruuidPK, FK → Users
roleintPKGlobal role level (not UUID — uses integer enum)
Source: schema.ts lines 1353–1360

Invitations

ColumnTypeConstraintsDescription
iduuidPK
emailcitextInvited email address
useruuidnullable, FK → UsersSet when invitee registers
organizationuuidnullable, FK → OrganizationsTarget organization
statusInviteStatusCurrent invitation state
Source: schema.ts lines 326–341 InviteStatus enum (common/src/types.ts lines 60–65):
  • invited = 1 — Email sent, awaiting action
  • registered = 2 — User created an account
  • joined = 3 — User accepted and joined the organization
  • bounced = 4 — Email delivery failed

MemberLimits

ColumnTypeConstraintsDescription
organizationuuidPK, FK → Organizations
memberLimitintMaximum members allowed for this organization’s plan
Source: schema.ts lines 883–889

Relationships


4. API Endpoints

Member Endpoints

MethodPathAuth GuardDescription
GET/organization/:organization/membercanViewMembersList all members
GET/organization/:organization/member/:membercanViewMemberGet single member
POST/organization/:organization/membercanEditMembersCreate new member
PATCH/organization/:organization/member/:membercanEditMembersUpdate member
DELETE/organization/:organization/member/:membercanEditMembersDelete member
DELETE/organization/:organization/user/:usercanEditMembersDelete member by linked user
POST/organization/:organization/member/invitecanEditMembersSend invitations
GET/organization/:organization/structure/member/limitcanViewOrganizationGet member limit
GET/organization/:organization/user/:usercanViewOrganizationGet user’s permissions in org
GET/entity/:entity/memberGet user’s member records across entities
Source: organization-endpoints.ts lines 132–236

Role Endpoints

MethodPathAuth GuardDescription
POST/organization/:organization/rolecanEditMembersCreate role
PUT/organization/:organization/role/:rolecanEditMembersUpdate role
GET/organization/:organization/rolecanViewMembersList roles
GET/organization/:organization/role/:rolecanViewMembersGet single role
DELETE/organization/:organization/role/:rolecanEditMembersDelete role
PUT/organization/:organization/role/:role/membercanEditMembersAssign members to role
Source: organization-endpoints.ts lines 274–304

Request/Response Schemas

// POST /organization/:organization/role
interface NewRoleRequest {
  organization: Uuid;
  role: string;        // role name
  description: string;
  permission: Uuid[];  // array of permission IDs
}

// PUT /organization/:organization/role/:role
interface UpdateRoleRequest {
  organization: Uuid;
  role: Uuid;
  newRole: string;      // updated role name
  description: string;
  permission: Uuid[];
}

// PUT /organization/:organization/role/:role/member
interface NewMemberRoleRequest {
  organization: Uuid;
  role: Uuid;
  member: Uuid[];  // member IDs to assign
}

// GET /organization/:organization/role/:role → response
interface Role {
  id: Uuid;
  name: string;
  description: string;
  permissions: Uuid[];
  isShared: boolean;
}

5. Frontend Components

Team Members Pages

ComponentFilePurpose
MembersPageteam-members/pages/team-members.tsxList all org members with invite/add actions
InviteMembersPageteam-members/pages/invite.tsxBulk invite members by email
NewMemberPageteam-members/pages/new-member-page.tsxCreate member form
EditMemberPageteam-members/pages/edit-member-page.tsxEdit member form
MemberPageteam-members/components/team-members-profile/team-member.tsxMember profile view

Team Members Components

ComponentFilePurpose
MemberFormteam-members/components/member-form.tsxShared create/edit form (9 fields)
InvitedMemberRowteam-members/components/invited-member-row.tsxPending invitation display

Roles Pages

ComponentFilePurpose
RolesPageroles/pages/roles.tsxList all roles
CreateRolePageroles/pages/create-role-page.tsxCreate new role
ViewRolePageroles/pages/view-role-page.tsxRole detail view
EditRolePageroles/pages/edit-role-page.tsxEdit role
PermissionsPageroles/pages/permissions-page.tsxOrganization permissions overview

Roles Components

ComponentFilePurpose
RoleFormroles/components/role-form.tsxShared create/edit role form
RolePermissionsTableroles/components/role-permissions-table.tsxPermission checkboxes for a role
PermissionsTableroles/components/permissions-table.tsxFull permissions matrix display

Routes

RouteComponent
/organization/:organization/membersMembersPage
/organization/:organization/member/inviteInviteMembersPage
/organization/:organization/member/newNewMemberPage
/organization/:organization/member/:member/editEditMemberPage
/organization/:organization/member/:memberMemberPage
/organization/:organization/rolesRolesPage
/organization/:organization/role/newCreateRolePage
/organization/:organization/role/:roleViewRolePage
/organization/:organization/role/:role/editEditRolePage
/organization/:organization/permissionsPermissionsPage

Member Form Fields

interface CommonEditableMemberFields {
  title: string;
  email: string;
  roles: Uuid[];
  dateOfBirth: Date;
  address: Address;
  types: 'individual' | 'organization';
}

interface EditableMemberFields extends CommonEditableMemberFields {
  fullName: string;
  phone: string;
  user: Uuid;
}

6. Business Rules and Validation

6.1 Permissions System

Frontend permissions (15 named, defined in roles/utility.ts lines 35–126):
PermissionControls
deleteDocumentsDelete files from data room and governing documents
editBillingManage subscriptions, payment profiles
editCapTableIssue shares, create security types, execute transfers
editDocumentsUpload, rename, move files
editIncentivePlanCreate/modify ESOP plans
editMembersAdd, edit, remove members and roles
editOrganizationDetailsChange org name, address, settings
signingSign certificates and agreements
viewGoverningDocumentsRead governing documents
viewCapTableView shareholdings, security types, valuations
viewDocumentsRead data room files
viewIncentivePlanView ESOP plans and grants
viewMembersView member list and profiles
viewOrganizationView organization details
viewSelfView own member profile
Backend-only permissions (4 additional, in common/src/permissions.ts, not exposed in frontend role UI):
  • fullVoting — Full voting rights
  • partialVoting — Partial voting rights
  • writeSite — Write access to organization site
  • readSite — Read access to organization site

6.2 Permission Enforcement

Permission checks are implemented in api/src/site/authorization.ts as guard functions:
  • canViewMember, canEditMembers, canViewMembers, canViewSomeMembers
  • canViewOrganization, canEditOrganization
  • canViewCapTable, canEditCapTable
  • canViewDocuments, canEditDocuments, canDeleteDocuments
  • And others for billing, incentive plans, signing, governing docs
Each endpoint declares its guard via the requires property. The guard checks the requesting user’s roles → permissions before allowing the request.

6.3 Invitation Flow

  1. Admin calls POST /organization/:organization/member/invite with member IDs
  2. Server queries members by organization + IDs, deduplicates by email
  3. For each unique email: creates Invitation record (status: invited), sends email via commonNotificationTemplates.organizationInvitation
  4. When invitee registers: status updates to registered
  5. When invitee accepts and joins: status updates to joined
  6. If email bounces: status updates to bounced
Source: organizations/src/invitations.ts lines 30–64

6.4 Member Limits

Organizations have a memberLimit set by their subscription plan. The GET /organization/:organization/structure/member/limit endpoint returns the current limit. The frontend displays a MemberLimitError component when the limit is reached. Source: equa-web/src/shared/errors/member-limit-error.tsx

6.5 Role Assignment Rules

  • Roles are scoped to an organization via OrganizationsRoles
  • Members are assigned to roles via MembersRoles (many-to-many)
  • A member can have multiple roles; permissions are the union of all assigned roles’ permissions
  • Roles can be shared (isShared = true) across organizations
  • GlobalRolesUsers assigns platform-wide roles (admin) using integer role levels, separate from org-level RBAC

7. Acceptance Criteria

  • Organization owner can create, edit, and delete custom roles with any combination of the 15 permissions
  • Members can be assigned multiple roles; effective permissions are the union of all roles
  • New members can be invited by email; invitation creates a record and sends an email notification
  • Invitation status progresses through invited → registered → joined (or bounced)
  • Member form captures all 9 editable fields (fullName, email, phone, title, dateOfBirth, address, user, roles, types)
  • Member limit is enforced: adding members beyond the limit shows the MemberLimitError
  • Permission guards on all 16 endpoints correctly deny access when the requesting user lacks the required permission
  • Deleting a member removes their role assignments
  • The 4 backend-only permissions (fullVoting, partialVoting, writeSite, readSite) do not appear in the frontend role editor
  • Role deletion removes all PermissionsRoles and MembersRoles join records for that role

8. Risks and Edge Cases

RiskImpactMitigation
Email case sensitivity on invitationsDuplicate invitations to same address with different casingemail column uses citext (case-insensitive text) type
Orphaned role assignments after member deletionStale MembersRoles rowsCascade delete or explicit cleanup on member removal
Member limit race conditionTwo concurrent invites exceed limitServer-side check in transaction before creating invitation
GlobalRolesUsers uses int, not UUIDPlatform admin role check differs from org-level RBACKeep separate code paths; do not mix global and org role queries
Shared roles modified by one org affect anotherUnintended permission changesisShared roles should be read-only for non-owner orgs
Backend-only permissions not visible in UIUsers cannot understand or manage voting/site permissionsDocument as internal/system permissions until frontend support is added

9. Dependencies

DependencyTypeStatus
001-Authentication (user sessions)Required beforeDRAFT
002-Organization ManagementRequired beforeDRAFT
011-Billing and Subscriptions (member limits)Integrates withDRAFT