Features
Organization
Team Members

Team Member Management

Comprehensive guide to managing team members and invitations in your OEC.SH organization.


Overview

Team member management in OEC.SH allows you to collaborate with your team by inviting members, assigning roles, and controlling access to your organization's resources. The platform supports both organization-wide and project-specific access through a flexible invitation system.

Key Features

  • Flexible Invitations: Invite members via email with organization-wide or project-specific access
  • Role-Based Access Control: 4 organization roles (Owner, Admin, Developer, Viewer) + permission matrix
  • Invitation Codes: 6-character codes or secure link tokens for easy onboarding
  • Multi-Use Invitations: Create invitations that can be used by multiple team members
  • Email Integration: Automatic invitation emails with onboarding links
  • Real-Time Updates: SSE events for instant UI updates when members join or leave
  • Audit Trail: Complete tracking of member additions, role changes, and removals

Organization Member Roles

OEC.SH uses a hierarchical role system for organization members. Each role has specific permissions and capabilities.

Role Hierarchy

OWNER > ADMIN > DEVELOPER > VIEWER

Role Definitions

Owner

  • Full Control: Complete access to all organization features
  • Billing Management: Can manage subscriptions, payment methods, and billing settings
  • Organization Deletion: Only role that can delete the organization
  • Ownership Transfer: Can transfer ownership to another member
  • Member Management: Add, remove, and change roles of any member
  • Resource Quotas: Manage organization-level resource quotas
  • Unique: Only one owner per organization (automatic on creation)

Permissions Include:

  • All Admin permissions
  • org.delete - Delete organization
  • org.billing.manage - Manage billing and subscriptions
  • org.ownership.transfer - Transfer ownership

Admin

  • Full Management: Manage all organization resources except billing and deletion
  • Team Management: Invite, remove members, and change roles (except Owner)
  • Settings Control: Update organization settings, DNS, storage configurations
  • Project Management: Create, update, delete projects
  • Server Management: Add and manage servers

Permissions Include:

  • All Developer permissions
  • org.settings.update - Update organization settings
  • org.members.invite - Invite new members
  • org.members.remove - Remove members
  • org.members.update_role - Change member roles
  • org.servers.create - Add new servers
  • org.servers.delete - Remove servers
  • org.dns.manage - Manage DNS configurations
  • org.storage.manage - Manage storage providers

Developer

  • Standard Access: Create and manage projects, deploy environments
  • Project Operations: Full CRUD on projects and environments
  • Deployment: Deploy, update, and manage application deployments
  • Backups: Create and restore backups
  • Environment Management: Create, configure, and delete environments

Permissions Include:

  • All Viewer permissions
  • org.projects.create - Create new projects
  • org.projects.update - Update project settings
  • org.projects.delete - Delete projects (own projects)
  • org.environments.create - Create environments
  • org.environments.deploy - Deploy environments
  • org.environments.update - Update environment configurations
  • org.environments.delete - Delete environments
  • org.backups.create - Create backups
  • org.backups.restore - Restore from backups

Viewer

  • Read-Only Access: View organization resources and configurations
  • No Modifications: Cannot create, update, or delete resources
  • Monitoring: View logs, metrics, and deployment status
  • Reports: Access usage reports and analytics

Permissions Include:

  • org.view - View organization details
  • org.members.list - View team members
  • org.projects.list - View projects
  • org.environments.list - View environments
  • org.logs.view - View application logs
  • org.monitoring.view - View metrics and monitoring data

Viewing Team Members

List Organization Members

View all active members of your organization with their roles and status.

Endpoint: GET /api/v1/organizations/{org_id}/members

Required Permission: org.members.list

Response:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "role": "ADMIN",
    "org_role": "org_admin",
    "managed_project_ids": [],
    "is_active": true,
    "user": {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "email": "admin@example.com",
      "name": "Jane Admin",
      "avatar_url": "https://gravatar.com/avatar/..."
    },
    "created_at": "2024-01-15T10:30:00Z"
  },
  {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "user_id": "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
    "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "role": "DEVELOPER",
    "org_role": "org_manager",
    "managed_project_ids": [
      "a1b2c3d4-e5f6-4321-a987-123456789abc"
    ],
    "is_active": true,
    "user": {
      "id": "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
      "email": "developer@example.com",
      "name": "John Developer",
      "avatar_url": null
    },
    "created_at": "2024-02-20T14:45:00Z"
  }
]

Member Information Fields

FieldTypeDescription
idUUIDUnique membership identifier
user_idUUIDUser ID of the member
organization_idUUIDOrganization ID
roleStringMember's role (OWNER, ADMIN, DEVELOPER, VIEWER)
org_roleStringInternal org role (org_admin, org_manager, org_user)
managed_project_idsArrayProject IDs this member can manage (for DEVELOPER role)
is_activeBooleanWhether membership is active
userObjectUser details (email, name, avatar)
created_atISO 8601When the member joined

Filtering and Search

The frontend typically provides:

  • Role Filter: Filter by OWNER, ADMIN, DEVELOPER, VIEWER
  • Status Filter: Active vs. Suspended members
  • Search: Search by name or email
  • Sort: By join date, name, or role

Inviting Team Members

OEC.SH supports two invitation scopes:

  1. Organization-Wide: Member gets access to all projects in the organization
  2. Project-Specific: Member gets access only to specified projects

Create Organization Invitation

Endpoint: POST /api/v1/invitations/organizations/{organization_id}

Required Permission: org.members.invite

Request Body:

{
  "email": "newmember@example.com",
  "scope": "organization",
  "role": "DEVELOPER",
  "expires_in_days": 7,
  "max_uses": 1,
  "message": "Welcome to our team! We're excited to have you join us."
}

Request Schema:

{
  email?: string | null;           // Restrict to specific email (optional)
  scope: "organization" | "project"; // Invitation scope
 
  // Organization-scope fields
  role: "OWNER" | "ADMIN" | "DEVELOPER" | "VIEWER"; // For org-wide invites
 
  // Project-scope fields (when scope = "project")
  project_ids?: string[];          // Required for project-scope
  project_role?: "admin" | "developer" | "viewer"; // Role within projects
 
  // Common fields
  expires_in_days: number;         // 1-30 days (default: 7)
  max_uses?: number | null;        // 1-100 or null for unlimited (default: 1)
  message?: string | null;         // Optional message (max 500 chars)
}

Response:

{
  "id": "inv-123e4567-e89b-12d3-a456-426614174000",
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "email": "newmember@example.com",
  "scope": "organization",
  "role": "DEVELOPER",
  "org_role": "org_manager",
  "project_ids": null,
  "project_role": null,
  "projects": null,
  "code": "ABC123",
  "link_token": "veryLongSecureTokenWith64CharactersForInvitationLinkSecurity",
  "status": "pending",
  "expires_at": "2024-12-25T10:30:00Z",
  "max_uses": 1,
  "use_count": 0,
  "remaining_uses": 1,
  "is_valid": true,
  "invited_by": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "inviter_name": "Jane Admin",
  "message": "Welcome to our team! We're excited to have you join us.",
  "created_at": "2024-12-18T10:30:00Z",
  "updated_at": "2024-12-18T10:30:00Z"
}

Invitation Codes

Every invitation has two ways to be used:

  1. 6-Character Code: Short code for manual entry (e.g., ABC123)

    • Uses characters: ABCDEFGHJKMNPQRSTUVWXYZ23456789
    • Excludes confusing characters: 0, O, I, L
    • Case-insensitive
  2. 64-Character Link Token: Secure token for invitation links

    • Used in URL: https://app.oec.sh/onboarding?token=...
    • More secure than code
    • Only shown once on creation

Invitation States

StateDescription
pendingWaiting to be accepted
acceptedSuccessfully accepted (when max_uses reached)
expiredPast expiration date
revokedManually cancelled by admin

Email Invitation Workflow

When an invitation is created with an email address and email service is configured:

  1. Invitation Created: Backend generates code + link token
  2. Email Queued: Task enqueued via ARQ for sending
  3. Email Sent: Email service sends invitation with:
    • Organization name
    • Inviter name
    • Invitation link with token
    • Expiration date
    • Optional message from inviter
    • Scope and role information
  4. User Receives Email: Contains clickable link to onboarding page
  5. User Clicks Link: Redirected to /onboarding?token=...
  6. Validation: Frontend validates token via API
  7. Acceptance: User registers/logs in and accepts invitation

Project-Specific Invitations

Invite users to specific projects only (not organization-wide access):

Request:

{
  "email": "contractor@example.com",
  "scope": "project",
  "project_ids": [
    "a1b2c3d4-e5f6-4321-a987-123456789abc",
    "b2c3d4e5-f6a1-5432-b098-234567890bcd"
  ],
  "project_role": "developer",
  "expires_in_days": 14,
  "max_uses": 1,
  "message": "Here's access to the Client XYZ project. Let me know if you need help!"
}

Response:

{
  "id": "inv-234f5678-f90c-23e4-b567-537725285111",
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "email": "contractor@example.com",
  "scope": "project",
  "role": "VIEWER",
  "org_role": "org_user",
  "project_ids": [
    "a1b2c3d4-e5f6-4321-a987-123456789abc",
    "b2c3d4e5-f6a1-5432-b098-234567890bcd"
  ],
  "project_role": "developer",
  "projects": [
    {
      "id": "a1b2c3d4-e5f6-4321-a987-123456789abc",
      "name": "Client XYZ Website",
      "slug": "client-xyz-website"
    },
    {
      "id": "b2c3d4e5-f6a1-5432-b098-234567890bcd",
      "name": "Client XYZ Backend",
      "slug": "client-xyz-backend"
    }
  ],
  "code": "DEF456",
  "link_token": "anotherVeryLongSecureTokenWith64CharactersForProjectInvite",
  "status": "pending",
  "expires_at": "2025-01-01T10:30:00Z",
  "max_uses": 1,
  "use_count": 0,
  "remaining_uses": 1,
  "is_valid": true,
  "invited_by": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "inviter_name": "Jane Admin",
  "message": "Here's access to the Client XYZ project. Let me know if you need help!",
  "created_at": "2024-12-18T10:30:00Z",
  "updated_at": "2024-12-18T10:30:00Z"
}

Accepting Invitations

Validate Invitation (Public)

Before accepting, users can validate an invitation code or token.

Endpoint: POST /api/v1/invitations/validate

Authentication: Not required (public endpoint)

Request:

{
  "code": "ABC123"
}

OR

{
  "token": "veryLongSecureTokenWith64Characters..."
}

Response (Valid):

{
  "valid": true,
  "organization_name": "Acme Corp",
  "organization_slug": "acme-corp",
  "email_restricted": true,
  "restricted_email": "newmember@example.com",
  "expires_at": "2024-12-25T10:30:00Z",
  "message": "Welcome to our team!",
  "scope": "organization",
  "projects": null,
  "project_role": null,
  "error": null
}

Response (Invalid):

{
  "valid": false,
  "organization_name": "Acme Corp",
  "organization_slug": "acme-corp",
  "email_restricted": true,
  "restricted_email": "newmember@example.com",
  "expires_at": "2024-12-18T10:30:00Z",
  "message": null,
  "scope": "organization",
  "projects": null,
  "project_role": null,
  "error": "Invitation has expired"
}

Accept Invitation

Endpoint: POST /api/v1/invitations/accept

Authentication: Required

Request:

{
  "code": "ABC123"
}

Response (Organization-Scope):

{
  "success": true,
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "organization_name": "Acme Corp",
  "organization_slug": "acme-corp",
  "scope": "organization",
  "role": "DEVELOPER",
  "org_role": "org_manager",
  "project_access": null,
  "message": "Welcome to Acme Corp!"
}

Response (Project-Scope):

{
  "success": true,
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "organization_name": "Acme Corp",
  "organization_slug": "acme-corp",
  "scope": "project",
  "role": null,
  "org_role": null,
  "project_access": [
    {
      "project_id": "a1b2c3d4-e5f6-4321-a987-123456789abc",
      "project_name": "Client XYZ Website",
      "role": "developer"
    },
    {
      "project_id": "b2c3d4e5-f6a1-5432-b098-234567890bcd",
      "project_name": "Client XYZ Backend",
      "role": "developer"
    }
  ],
  "message": "You now have access to 2 project(s) in Acme Corp!"
}

Acceptance Flow

  1. User Receives Invitation: Via email or shared link/code
  2. User Navigates: To /onboarding?token=... or enters code manually
  3. Validation: Frontend validates invitation (public endpoint)
  4. Registration/Login: User registers if new, or logs in if existing
  5. Acceptance: Authenticated user accepts invitation
  6. Membership Created:
    • Organization-scope: Creates OrganizationMember record
    • Project-scope: Creates ProjectMember record(s)
  7. Use Count Incremented: use_count++ on invitation
  8. Status Update: If use_count >= max_uses, status → accepted
  9. Welcome Email: Sent via task queue
  10. Redirect: User redirected to organization dashboard

Email Restrictions

If email field is set on invitation:

  • Only that specific email can accept
  • Case-insensitive matching
  • Prevents invitation sharing
  • Error if wrong email tries to accept: "This invitation is restricted to user@example.com"

Multi-Use Invitations

Create invitations for team onboarding:

{
  "email": null,              // No email restriction
  "scope": "organization",
  "role": "DEVELOPER",
  "max_uses": 10,             // Can be used 10 times
  "expires_in_days": 30
}

Use Cases:

  • Onboarding multiple team members
  • Conference/event access codes
  • Partner access
  • Temporary contractor access

Tracking:

  • use_count: Number of times accepted
  • remaining_uses: max_uses - use_count
  • is_valid: false when use_count >= max_uses

Managing Member Roles

Update Member Role

Change a member's organization role.

Endpoint: PATCH /api/v1/organizations/{org_id}/members/{user_id}

Required Permission: org.members.update_role

Request:

{
  "role": "ADMIN",
  "managed_project_ids": []
}

Response:

{
  "id": "660e8400-e29b-41d4-a716-446655440001",
  "user_id": "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "role": "ADMIN",
  "org_role": "org_admin",
  "managed_project_ids": [],
  "is_active": true,
  "user": {
    "id": "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
    "email": "developer@example.com",
    "name": "John Developer",
    "avatar_url": null
  },
  "created_at": "2024-02-20T14:45:00Z"
}

Role Change Restrictions

Cannot Change Owner's Role:

// Error response
{
  "detail": "Cannot change the owner's legacy role"
}

Permission Required:

  • Must have org.members.update_role permission
  • Typically limited to OWNER and ADMIN roles

Managed Projects (DEVELOPER Role)

Developers can be restricted to specific projects:

{
  "role": "DEVELOPER",
  "managed_project_ids": [
    "a1b2c3d4-e5f6-4321-a987-123456789abc",
    "b2c3d4e5-f6a1-5432-b098-234567890bcd"
  ]
}

Use Cases:

  • Client-specific access
  • Department separation
  • Contractor isolation
  • Security isolation

Removing Team Members

Remove Member from Organization

Deactivate a member's access to the organization.

Endpoint: DELETE /api/v1/organizations/{org_id}/members/{user_id}

Required Permission: org.members.remove

Response:

{
  "message": "Member removed successfully"
}

Removal Restrictions

Cannot Remove Owner:

{
  "detail": "Cannot remove the organization owner"
}

Cannot Remove Self:

  • Use "Leave Organization" endpoint instead
  • Prevents accidental self-removal

Soft Delete Behavior

Member removal is a soft delete:

  • Sets is_active = false on OrganizationMember record
  • Preserves audit trail and resource ownership
  • Can be reactivated by adding member again
  • Member loses all access immediately

Resource Ownership Transfer

Before removing a member who owns resources:

  1. Check Ownership: Verify if member owns projects, environments, servers
  2. Transfer Ownership: Reassign resources to another member
  3. Remove Member: Proceed with removal once resources are transferred

Leaving an Organization

Members can leave an organization voluntarily (except Owner).

Endpoint: POST /api/v1/organizations/{org_id}/leave

Authentication: Required

Response:

{
  "message": "You have left the organization successfully"
}

Leave Restrictions

Owner Cannot Leave:

{
  "detail": "Organization owner cannot leave. Transfer ownership first."
}

Ownership Transfer Requirement:

  1. Owner must transfer ownership to another ADMIN or OWNER
  2. Once transferred, original owner becomes ADMIN
  3. Then they can leave the organization

Suspending Members

Temporarily disable member access without removing them.

Endpoint: PATCH /api/v1/organizations/{org_id}/members/{user_id}/suspend

Required Permission: org.members.update

Request:

{
  "suspended": true,
  "reason": "Security review in progress"
}

Suspension vs. Removal

ActionEffectReversibleAudit TrailResource Ownership
SuspendTemporary access lossYes (reactivate)PreservedMaintained
RemovePermanent access lossYes (re-invite)PreservedTransferred

Use Cases for Suspension

  • Security Incidents: Immediate access revocation during investigation
  • Offboarding Process: Gradual access reduction before full removal
  • Policy Violations: Temporary suspension pending review
  • Inactive Accounts: Disable unused accounts without deletion
  • Leave of Absence: Maintain membership but disable access

Reactivating Suspended Members

Endpoint: PATCH /api/v1/organizations/{org_id}/members/{user_id}/suspend

Request:

{
  "suspended": false
}

Invitation Management

List Pending Invitations

View all invitations for an organization.

Endpoint: GET /api/v1/invitations/organizations/{organization_id}

Required Permission: org.invitations.list

Query Parameters:

  • status (optional): Filter by pending, accepted, expired, revoked
  • include_expired (boolean): Include expired invitations (default: false)

Response:

{
  "invitations": [
    {
      "id": "inv-123e4567-e89b-12d3-a456-426614174000",
      "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "email": "newmember@example.com",
      "scope": "organization",
      "role": "DEVELOPER",
      "org_role": "org_manager",
      "code": "ABC123",
      "link_token": null,
      "status": "pending",
      "expires_at": "2024-12-25T10:30:00Z",
      "max_uses": 1,
      "use_count": 0,
      "remaining_uses": 1,
      "is_valid": true,
      "invited_by": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "inviter_name": "Jane Admin",
      "message": "Welcome to our team!",
      "created_at": "2024-12-18T10:30:00Z",
      "updated_at": "2024-12-18T10:30:00Z"
    }
  ],
  "total": 1
}

Get Specific Invitation

Endpoint: GET /api/v1/invitations/organizations/{organization_id}/{invitation_id}

Required Permission: org.invitations.list

Resend Invitation Email

Send invitation email again to the same recipient.

Endpoint: POST /api/v1/invitations/organizations/{organization_id}/{invitation_id}/resend

Required Permission: org.members.invite

Restrictions:

  • Only pending invitations can be resent
  • Invitation must not be expired
  • Invitation must have an email address
  • Email service must be configured

Response:

{
  "id": "inv-123e4567-e89b-12d3-a456-426614174000",
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "email": "newmember@example.com",
  "scope": "organization",
  "role": "DEVELOPER",
  "code": "ABC123",
  "status": "pending",
  "expires_at": "2024-12-25T10:30:00Z",
  "is_valid": true,
  "message": "Invitation email resent successfully"
}

Revoke Invitation

Cancel a pending invitation.

Endpoint: DELETE /api/v1/invitations/organizations/{organization_id}/{invitation_id}

Required Permission: org.invitations.revoke

Restrictions:

  • Only pending invitations can be revoked
  • Once revoked, invitation cannot be accepted

Response: 204 No Content

Cleanup Expired Invitations

Delete all expired and revoked invitations from an organization.

Endpoint: DELETE /api/v1/invitations/organizations/{organization_id}/cleanup

Required Permission: org.invitations.revoke

Response:

{
  "deleted_count": 5
}

User's Received Invitations

List My Pending Invitations

View all invitations sent to the current user's email.

Endpoint: GET /api/v1/invitations/me

Authentication: Required

Response:

{
  "invitations": [
    {
      "id": "inv-234f5678-f90c-23e4-b567-537725285111",
      "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "email": "myemail@example.com",
      "scope": "organization",
      "role": "DEVELOPER",
      "code": "XYZ789",
      "status": "pending",
      "expires_at": "2025-01-15T10:30:00Z",
      "is_valid": true,
      "invited_by": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "inviter_name": "Jane Admin",
      "message": "Join our team!",
      "created_at": "2024-12-10T10:30:00Z"
    }
  ],
  "total": 1
}

Filters:

  • Only shows invitations restricted to user's email
  • Only shows pending status
  • Only shows non-expired invitations
  • Only shows invitations not at max uses

Member Activity Tracking

Last Active Timestamp

Track when members last accessed the platform:

{
  user: {
    id: string;
    email: string;
    name: string;
    last_login_at: string; // ISO 8601 timestamp
  }
}

Activity Audit Trail

All member actions are logged in the audit system:

  • Member Added: ORG_MEMBER_ADDED
  • Role Changed: ORG_MEMBER_ROLE_CHANGED
  • Member Removed: ORG_MEMBER_REMOVED
  • Invitation Created: INVITATION_CREATED
  • Invitation Accepted: INVITATION_ACCEPTED
  • Invitation Revoked: INVITATION_REVOKED
  • Invitation Resent: INVITATION_RESENT

Example Audit Entry:

{
  "id": "log-123",
  "action": "org_member_added",
  "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "resource_type": "organization_member",
  "resource_id": "660e8400-e29b-41d4-a716-446655440001",
  "resource_name": "developer@example.com",
  "details": {
    "member_user_id": "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
    "member_email": "developer@example.com",
    "role": "DEVELOPER",
    "org_role": "org_manager"
  },
  "ip_address": "192.168.1.1",
  "user_agent": "Mozilla/5.0...",
  "created_at": "2024-12-18T10:30:00Z"
}

Permission System Integration

OEC.SH uses a dual permission system:

1. Legacy Role-Based Checks

Organization Member Roles:

  • Checked via OrganizationMember.role column
  • Simple enum: OWNER, ADMIN, DEVELOPER, VIEWER

2. Permission Matrix (Sprint 2E21)

Granular Permissions:

  • 55+ granular permissions with resource.action format
  • Example: org.members.invite, org.projects.create
  • Supports custom roles and permission overrides
  • Redis-cached abilities (5-minute TTL)

Common Member Management Permissions:

PermissionDescriptionDefault Roles
org.members.listView team membersAll
org.members.inviteSend invitationsOWNER, ADMIN
org.members.update_roleChange member rolesOWNER, ADMIN
org.members.removeRemove membersOWNER, ADMIN
org.invitations.listView pending invitationsOWNER, ADMIN, DEVELOPER
org.invitations.revokeCancel invitationsOWNER, ADMIN

Permission Checks in Code

Backend:

from core.permissions import check_permission
 
has_permission = await check_permission(
    db=db,
    user=current_user,
    permission_code="org.members.invite",
    organization_id=organization_id,
)
 
if not has_permission:
    raise HTTPException(403, "You don't have permission to invite members.")

Frontend:

import { useAbilities } from '@/hooks/useAbilities';
 
function MemberManagement({ organizationId }) {
  const { can } = useAbilities({ organizationId });
 
  return (
    <>
      {can('members', 'invite') && (
        <Button onClick={onInvite}>Invite Member</Button>
      )}
    </>
  );
}

SSE Real-Time Events

Member changes trigger Server-Sent Events for real-time UI updates.

Event Types

permissions_changed

Broadcasted when a user's permissions are updated.

{
  event_type: "permissions_changed",
  data: {
    user_id: "8d0e7780-8536-51ef-b058-f18fd3c4f91b",
    message: "Your permissions have been updated"
  }
}

Frontend Handling:

useEffect(() => {
  if (event?.event_type === 'permissions_changed') {
    // Refresh user abilities
    mutateAbilities();
    toast.info('Your permissions have been updated');
  }
}, [event]);

role_permissions_changed

Broadcasted when a role's permissions are modified.

{
  event_type: "role_permissions_changed",
  data: {
    role_id: "role-123",
    role_name: "Developer",
    message: "Permissions for role 'Developer' have been updated"
  }
}

SSE Connection

Endpoint: GET /api/v1/events/stream

Query Parameters:

  • organization_id (optional): Filter events to specific organization

Connection Headers:

Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Event Stream Format:

event: message
data: {"event_type":"permissions_changed","data":{...}}

event: message
data: {"event_type":"role_permissions_changed","data":{...}}

API Reference

Complete Endpoint List

Organization Members

MethodEndpointPermissionDescription
GET/organizations/{org_id}/membersorg.members.listList all members
POST/organizations/{org_id}/membersorg.members.inviteAdd member directly
PATCH/organizations/{org_id}/members/{user_id}org.members.update_roleUpdate member role
DELETE/organizations/{org_id}/members/{user_id}org.members.removeRemove member
POST/organizations/{org_id}/leave-Leave organization

Invitations - Organization Context

MethodEndpointPermissionDescription
POST/invitations/organizations/{org_id}org.members.inviteCreate invitation
GET/invitations/organizations/{org_id}org.invitations.listList invitations
GET/invitations/organizations/{org_id}/{inv_id}org.invitations.listGet invitation
POST/invitations/organizations/{org_id}/{inv_id}/resendorg.members.inviteResend invitation
DELETE/invitations/organizations/{org_id}/{inv_id}org.invitations.revokeRevoke invitation
DELETE/invitations/organizations/{org_id}/cleanuporg.invitations.revokeCleanup expired

Invitations - Public

MethodEndpointAuthDescription
POST/invitations/validateNoValidate invitation
POST/invitations/acceptYesAccept invitation
GET/invitations/meYesList my invitations

Request/Response Schemas

OrganizationMemberCreate

{
  email: string;                    // User email to add
  role: "OWNER" | "ADMIN" | "DEVELOPER" | "VIEWER";
  managed_project_ids?: string[];   // For DEVELOPER role
}

OrganizationMemberUpdate

{
  role?: "OWNER" | "ADMIN" | "DEVELOPER" | "VIEWER";
  managed_project_ids?: string[];
}

InvitationCreate

{
  email?: string | null;
  scope: "organization" | "project";
 
  // Organization-scope
  role: "OWNER" | "ADMIN" | "DEVELOPER" | "VIEWER";
 
  // Project-scope
  project_ids?: string[];
  project_role?: "admin" | "developer" | "viewer";
 
  // Common
  expires_in_days: number;          // 1-30
  max_uses?: number | null;         // 1-100 or null
  message?: string | null;          // Max 500 chars
}

InvitationValidate

{
  code?: string;   // 6 characters
  token?: string;  // 64 characters
}

InvitationAccept

{
  code?: string;   // 6 characters
  token?: string;  // 64 characters
}

Best Practices

Security

  1. Principle of Least Privilege

    • Assign minimum necessary role
    • Use DEVELOPER for most team members
    • Reserve ADMIN for trusted leadership
    • Limit OWNER to one or two people
  2. Regular Access Reviews

    • Monthly review of active members
    • Quarterly review of member roles
    • Remove inactive members
    • Audit permission changes
  3. Invitation Hygiene

    • Use email-restricted invitations for known users
    • Set short expiration (7 days) for security-sensitive orgs
    • Single-use invitations for individual onboarding
    • Cleanup expired invitations regularly
  4. Multi-Factor Authentication

    • Require 2FA for OWNER and ADMIN roles
    • Encourage 2FA for all members
    • Enforce 2FA via organization policy

Onboarding Workflow

  1. Prepare Resources

    • Create necessary projects before inviting
    • Set up project-specific roles if needed
    • Document team guidelines and processes
  2. Send Invitation

    • Use organization-scope for full access
    • Use project-scope for contractors/external
    • Include welcome message with next steps
    • Set appropriate expiration (7-14 days)
  3. Track Acceptance

    • Monitor pending invitations
    • Resend if not accepted within 3 days
    • Contact directly if invitation expires
  4. Verify Access

    • Confirm member can access resources
    • Check role permissions are correct
    • Provide onboarding documentation

Offboarding Workflow

  1. Transfer Ownership

    • Identify resources owned by departing member
    • Reassign project ownership
    • Transfer server management
  2. Revoke Access

    • Remove from organization
    • Verify all access revoked
    • Check for remaining API keys or tokens
  3. Audit Trail

    • Document removal in audit log
    • Review recent activity
    • Archive member's work if needed

Role Assignment Guidelines

Team Member TypeRecommended RoleReason
Founder/CEOOWNERFull control, billing
CTO/Tech LeadADMINManagement, no billing
DeveloperDEVELOPERDeploy, manage projects
DesignerDEVELOPERManage design projects
DevOpsADMINServer + project management
QA/TesterDEVELOPERAccess to test environments
Project ManagerVIEWERMonitor progress
Client/StakeholderVIEWERRead-only monitoring
ContractorProject-scope DEVELOPERLimited to specific projects
External PartnerProject-scope VIEWERRead-only project access

Troubleshooting

Invitation Not Received

Problem: User didn't receive invitation email

Solutions:

  1. Check spam/junk folder
  2. Verify email address is correct in invitation
  3. Check email service is configured on platform
  4. Use "Resend Invitation" button
  5. Share invitation code/link manually
  6. Check email delivery logs in audit trail

Cannot Remove Member

Problem: "Cannot remove the organization owner"

Solutions:

  1. Transfer ownership to another ADMIN first
  2. Only OWNER can transfer ownership
  3. After transfer, original owner becomes ADMIN
  4. Then remove the (now) ADMIN member

Problem: "Cannot remove member - last owner"

Solutions:

  1. Must have at least one OWNER
  2. Add another OWNER first
  3. Then remove the original owner

Permission Denied Errors

Problem: "You don't have permission to invite members"

Solutions:

  1. Check your role (need OWNER or ADMIN)
  2. Contact organization owner
  3. Verify org.members.invite permission
  4. Check permission cache (wait 5 minutes for refresh)

Problem: "Cannot access members from other organizations"

Solutions:

  1. Ensure you're in the correct organization context
  2. Verify organization_id in request
  3. Check you're a member of that organization
  4. Don't try to access cross-org members

Invitation Validation Errors

Problem: "Invitation has expired"

Solutions:

  1. Request new invitation from admin
  2. Admin can create new invitation with same email
  3. Set longer expiration (up to 30 days)

Problem: "Invitation has reached maximum uses"

Solutions:

  1. Multi-use invitation is exhausted
  2. Admin needs to create new invitation
  3. Check use_count and max_uses fields

Problem: "This invitation is restricted to email@example.com"

Solutions:

  1. You must use the specified email address
  2. Register/login with correct email
  3. Request new invitation for your email

Email Delivery Issues

Problem: Email service not configured

Solutions:

  1. Platform admin must configure SMTP settings
  2. Set EMAIL_ENABLED=true in environment
  3. Configure email templates
  4. Test email delivery

Problem: Email goes to spam

Solutions:

  1. Configure SPF, DKIM, DMARC records
  2. Use reputable email service (SendGrid, Mailgun)
  3. Whitelist sender domain
  4. Use custom domain for emails

Related Documentation


Last Updated: December 2024 Sprint: 2E21 - Permission Matrix System