Skip to main content

Member Management

Team Features

List, search, and manage organization members. Handle role changes, suspensions, removals, and bulk operations.

Overview

Effective member management is critical for team collaboration. The SDK provides comprehensive tools for listing, searching, updating, and removing organization members.

Member Listing

Paginated member list with role and status

Search & Filter

Find members by name, email, or role

Role Management

Update member roles with audit trail

Member Removal

Remove members with data handling options

Bulk Operations

Update or remove multiple members at once

Activity Tracking

See when members last accessed the org

Listing Members

Access the organization's member list with the useMembers hook or via the organization object.

import { useMembers } from '@sylphx/sdk/react'

function MemberList() {
  const {
    members,
    isLoading,
    error,
    totalCount,

    // Pagination
    page,
    pageSize,
    setPage,

    // Search & Filter
    search,
    setSearch,
    roleFilter,
    setRoleFilter,

    // Refresh
    refresh,
  } = useMembers({
    initialPageSize: 20,
  })

  if (isLoading) return <LoadingSpinner />
  if (error) return <ErrorMessage error={error} />

  return (
    <div>
      {/* Search & Filter */}
      <div className="flex gap-4 mb-6">
        <Input
          placeholder="Search by name or email..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          icon={<Search className="w-4 h-4" />}
        />

        <Select value={roleFilter} onValueChange={setRoleFilter}>
          <SelectItem value="all">All Roles</SelectItem>
          <SelectItem value="admin">Admins</SelectItem>
          <SelectItem value="developer">Developers</SelectItem>
          <SelectItem value="viewer">Viewers</SelectItem>
        </Select>
      </div>

      {/* Member List */}
      <div className="space-y-2">
        {members.map((member) => (
          <MemberRow key={member.id} member={member} />
        ))}
      </div>

      {/* Pagination */}
      <Pagination
        page={page}
        pageSize={pageSize}
        total={totalCount}
        onPageChange={setPage}
      />
    </div>
  )
}

Member Data Model

Understanding the membership data structure helps when building custom member UIs.

PropertyTypeDescription
idstringUnique membership ID
userIdstringUser ID of the member
organizationIdstringOrganization ID
roleOrganizationRoleMember role in this organization
joinedAtDateWhen the member joined
invitedBystring | nullUser ID who invited this member
status"active" | "suspended"Member status
userUserAssociated user data
user.idstringUser ID
user.emailstringUser email address
user.namestring | nullUser display name
user.avatarUrlstring | nullUser avatar URL
user.lastActiveAtDate | nullLast activity timestamp
Type Definition
interface Membership {
  id: string
  userId: string
  organizationId: string
  role: OrganizationRole
  joinedAt: Date
  invitedBy: string | null
  status: 'active' | 'suspended'

  user: {
    id: string
    email: string
    name: string | null
    avatarUrl: string | null
    lastActiveAt: Date | null
  }
}

type OrganizationRole =
  | 'super_admin'
  | 'admin'
  | 'billing'
  | 'analytics'
  | 'developer'
  | 'viewer'

Updating Member Roles

Change a member's role within the organization. Role changes are logged in the audit trail.

Role Update
import { useOrganization } from '@sylphx/sdk/react'

function MemberActions({ member }: { member: Membership }) {
  const { updateMemberRole, isAdmin } = useOrganization()
  const [isUpdating, setIsUpdating] = useState(false)

  const handleRoleChange = async (newRole: OrganizationRole) => {
    // Confirm significant role changes
    if (newRole === 'super_admin') {
      const confirmed = await confirm({
        title: 'Promote to Super Admin?',
        description: `This will give ${member.user.name} full administrative access including the ability to delete the organization.`,
        confirmText: 'Promote',
        variant: 'warning',
      })
      if (!confirmed) return
    }

    setIsUpdating(true)
    try {
      await updateMemberRole(member.userId, newRole)

      toast.success(`Updated ${member.user.name}'s role to ${newRole}`)

      // Track for audit
      analytics.track('member_role_changed', {
        memberId: member.userId,
        oldRole: member.role,
        newRole,
      })
    } catch (error) {
      handleRoleChangeError(error)
    } finally {
      setIsUpdating(false)
    }
  }

  const handleRoleChangeError = (error: { code?: string }) => {
    switch (error.code) {
      case 'CANNOT_CHANGE_OWNER':
        toast.error('Cannot change the organization owner\'s role')
        break
      case 'CANNOT_CHANGE_OWN_ROLE':
        toast.error('You cannot change your own role')
        break
      case 'INSUFFICIENT_PERMISSIONS':
        toast.error('You don\'t have permission to assign this role')
        break
      case 'LAST_ADMIN':
        toast.error('Cannot demote the last admin')
        break
      default:
        toast.error('Failed to update role')
    }
  }

  // Don't show role editor if not admin
  if (!isAdmin) {
    return <Badge>{member.role}</Badge>
  }

  return (
    <Select
      value={member.role}
      onValueChange={handleRoleChange}
      disabled={isUpdating}
    >
      <SelectItem value="super_admin">Super Admin</SelectItem>
      <SelectItem value="admin">Admin</SelectItem>
      <SelectItem value="billing">Billing</SelectItem>
      <SelectItem value="analytics">Analytics</SelectItem>
      <SelectItem value="developer">Developer</SelectItem>
      <SelectItem value="viewer">Viewer</SelectItem>
    </Select>
  )
}

Removing Members

Remove members from the organization. Handle data transfer and cleanup appropriately.

import { useOrganization } from '@sylphx/sdk/react'

function RemoveMemberButton({ member }: { member: Membership }) {
  const { removeMember, isAdmin } = useOrganization()
  const [isRemoving, setIsRemoving] = useState(false)

  const handleRemove = async () => {
    const confirmed = await confirm({
      title: 'Remove Member',
      description: `Remove ${member.user.name || member.user.email} from this organization? They will lose access to all organization resources.`,
      confirmText: 'Remove',
      variant: 'destructive',
    })

    if (!confirmed) return

    setIsRemoving(true)
    try {
      await removeMember(member.userId, {
        // Data handling options
        transferDataTo: null,     // Or another member ID
        deleteUserData: false,    // Keep their data for records
        notifyUser: true,         // Send removal notification
      })

      toast.success(`Removed ${member.user.name} from organization`)
    } catch (error) {
      if (error.code === 'CANNOT_REMOVE_OWNER') {
        toast.error('Cannot remove the organization owner')
      } else if (error.code === 'CANNOT_REMOVE_SELF') {
        toast.error('Cannot remove yourself. Use "Leave" instead.')
      } else {
        toast.error('Failed to remove member')
      }
    } finally {
      setIsRemoving(false)
    }
  }

  if (!isAdmin) return null

  return (
    <Button
      variant="ghost"
      size="sm"
      onClick={handleRemove}
      disabled={isRemoving}
      className="text-destructive"
    >
      <Trash2 className="w-4 h-4" />
    </Button>
  )
}

Suspending Members

Temporarily suspend member access without removing them from the organization.

Suspend/Unsuspend Member
import { useOrganization } from '@sylphx/sdk/react'

function MemberSuspension({ member }: { member: Membership }) {
  const { suspendMember, unsuspendMember, isAdmin } = useOrganization()
  const [isUpdating, setIsUpdating] = useState(false)

  const handleSuspend = async () => {
    const confirmed = await confirm({
      title: 'Suspend Member?',
      description: `${member.user.name} will immediately lose access to the organization. They will remain a member but cannot access any resources.`,
      confirmText: 'Suspend',
      variant: 'warning',
    })

    if (!confirmed) return

    setIsUpdating(true)
    try {
      await suspendMember(member.userId, {
        reason: 'Administrative action',
        notifyUser: true,
        // Optionally set auto-unsuspend
        unsuspendAt: null, // or new Date('2024-12-31')
      })

      toast.success(`${member.user.name} has been suspended`)
    } catch (error) {
      toast.error('Failed to suspend member')
    } finally {
      setIsUpdating(false)
    }
  }

  const handleUnsuspend = async () => {
    setIsUpdating(true)
    try {
      await unsuspendMember(member.userId, {
        notifyUser: true,
      })

      toast.success(`${member.user.name} has been unsuspended`)
    } catch (error) {
      toast.error('Failed to unsuspend member')
    } finally {
      setIsUpdating(false)
    }
  }

  if (!isAdmin) return null

  if (member.status === 'suspended') {
    return (
      <Button
        variant="outline"
        size="sm"
        onClick={handleUnsuspend}
        disabled={isUpdating}
      >
        <RefreshCw className="w-4 h-4 mr-2" />
        Unsuspend
      </Button>
    )
  }

  return (
    <Button
      variant="ghost"
      size="sm"
      onClick={handleSuspend}
      disabled={isUpdating}
    >
      <Ban className="w-4 h-4 mr-2" />
      Suspend
    </Button>
  )
}

Suspension vs Removal

Suspension temporarily blocks access but preserves the membership. Use this for investigations or temporary restrictions. Removal permanently removes the member and may delete their data.

Bulk Operations

Perform operations on multiple members at once for efficient team management.

import { useOrganization } from '@sylphx/sdk/react'

function BulkRoleUpdate() {
  const { bulkUpdateRoles } = useOrganization()
  const [selectedMembers, setSelectedMembers] = useState<string[]>([])
  const [newRole, setNewRole] = useState<OrganizationRole>('developer')

  const handleBulkUpdate = async () => {
    if (selectedMembers.length === 0) return

    const confirmed = await confirm({
      title: 'Update Roles',
      description: `Update ${selectedMembers.length} members to ${newRole}?`,
      confirmText: 'Update All',
    })

    if (!confirmed) return

    try {
      const result = await bulkUpdateRoles(selectedMembers, newRole)

      toast.success(`Updated ${result.updated} members`)
      if (result.skipped > 0) {
        toast.warning(`${result.skipped} members skipped (insufficient permissions)`)
      }

      setSelectedMembers([])
    } catch (error) {
      toast.error('Bulk update failed')
    }
  }

  return (
    <div className="flex items-center gap-4 p-4 bg-muted rounded-lg">
      <span className="text-sm text-muted-foreground">
        {selectedMembers.length} selected
      </span>

      <Select value={newRole} onValueChange={setNewRole}>
        <SelectItem value="admin">Admin</SelectItem>
        <SelectItem value="developer">Developer</SelectItem>
        <SelectItem value="viewer">Viewer</SelectItem>
      </Select>

      <Button onClick={handleBulkUpdate}>
        Update Roles
      </Button>
    </div>
  )
}

Member Activity

Track when members last accessed the organization to identify inactive accounts.

Activity Display
import { useMembers } from '@sylphx/sdk/react'
import { formatRelativeTime } from '@/lib/formatting'

function MemberActivity() {
  const { members } = useMembers()

  // Sort by last active
  const sortedByActivity = [...members].sort((a, b) => {
    const aTime = a.user.lastActiveAt?.getTime() || 0
    const bTime = b.user.lastActiveAt?.getTime() || 0
    return bTime - aTime
  })

  // Identify inactive members (no activity in 30 days)
  const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000
  const inactiveMembers = members.filter(m => {
    const lastActive = m.user.lastActiveAt?.getTime() || 0
    return lastActive < thirtyDaysAgo
  })

  return (
    <div>
      {/* Activity Warning */}
      {inactiveMembers.length > 0 && (
        <Alert variant="warning" className="mb-6">
          <AlertTitle>Inactive Members</AlertTitle>
          <AlertDescription>
            {inactiveMembers.length} members haven't been active in 30+ days.
            Consider reviewing their access.
          </AlertDescription>
        </Alert>
      )}

      {/* Member List with Activity */}
      <div className="space-y-2">
        {sortedByActivity.map((member) => (
          <div key={member.id} className="flex items-center justify-between p-4 border rounded-lg">
            <div className="flex items-center gap-3">
              <Avatar src={member.user.avatarUrl} />
              <div>
                <p className="font-medium">{member.user.name || member.user.email}</p>
                <p className="text-sm text-muted-foreground">{member.role}</p>
              </div>
            </div>

            <div className="text-sm text-muted-foreground">
              {member.user.lastActiveAt ? (
                <span className="flex items-center gap-1">
                  <Clock className="w-3 h-3" />
                  {formatRelativeTime(member.user.lastActiveAt)}
                </span>
              ) : (
                <span className="text-warning">Never active</span>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Complete Member Management UI

A complete example combining all member management features.

Full Member Management Page
'use client'

import { useMembers, useOrganization } from '@sylphx/sdk/react'
import { useState } from 'react'

export function MemberManagement() {
  const { isAdmin, isSuperAdmin } = useOrganization()
  const {
    members,
    isLoading,
    search,
    setSearch,
    roleFilter,
    setRoleFilter,
    page,
    setPage,
    totalCount,
    refresh,
  } = useMembers({ initialPageSize: 20 })

  const [selectedMembers, setSelectedMembers] = useState<Set<string>>(new Set())
  const [bulkAction, setBulkAction] = useState<string | null>(null)

  const toggleSelect = (memberId: string) => {
    const newSelected = new Set(selectedMembers)
    if (newSelected.has(memberId)) {
      newSelected.delete(memberId)
    } else {
      newSelected.add(memberId)
    }
    setSelectedMembers(newSelected)
  }

  const selectAll = () => {
    if (selectedMembers.size === members.length) {
      setSelectedMembers(new Set())
    } else {
      setSelectedMembers(new Set(members.map(m => m.userId)))
    }
  }

  if (isLoading) {
    return <MemberListSkeleton />
  }

  return (
    <div className="space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">Team Members</h1>
          <p className="text-muted-foreground">
            {totalCount} members in this organization
          </p>
        </div>

        {isAdmin && (
          <Button onClick={() => router.push('/settings/members/invite')}>
            <UserPlus className="w-4 h-4 mr-2" />
            Invite Members
          </Button>
        )}
      </div>

      {/* Filters */}
      <div className="flex flex-wrap gap-4">
        <div className="flex-1 min-w-[200px]">
          <Input
            placeholder="Search members..."
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            icon={<Search className="w-4 h-4" />}
          />
        </div>

        <Select value={roleFilter} onValueChange={setRoleFilter}>
          <SelectItem value="all">All Roles</SelectItem>
          <SelectItem value="super_admin">Super Admin</SelectItem>
          <SelectItem value="admin">Admin</SelectItem>
          <SelectItem value="billing">Billing</SelectItem>
          <SelectItem value="analytics">Analytics</SelectItem>
          <SelectItem value="developer">Developer</SelectItem>
          <SelectItem value="viewer">Viewer</SelectItem>
        </Select>

        <Button variant="outline" onClick={refresh}>
          <RefreshCw className="w-4 h-4" />
        </Button>
      </div>

      {/* Bulk Actions */}
      {selectedMembers.size > 0 && isAdmin && (
        <div className="flex items-center gap-4 p-4 bg-muted rounded-lg">
          <span className="text-sm font-medium">
            {selectedMembers.size} selected
          </span>
          <Button variant="outline" size="sm" onClick={() => setBulkAction('role')}>
            Change Role
          </Button>
          {isSuperAdmin && (
            <Button variant="destructive" size="sm" onClick={() => setBulkAction('remove')}>
              Remove
            </Button>
          )}
          <Button variant="ghost" size="sm" onClick={() => setSelectedMembers(new Set())}>
            Clear Selection
          </Button>
        </div>
      )}

      {/* Member Table */}
      <div className="border rounded-lg overflow-hidden">
        <table className="w-full">
          <thead className="bg-muted">
            <tr>
              {isAdmin && (
                <th className="p-4 text-left">
                  <Checkbox
                    checked={selectedMembers.size === members.length}
                    onCheckedChange={selectAll}
                  />
                </th>
              )}
              <th className="p-4 text-left">Member</th>
              <th className="p-4 text-left">Role</th>
              <th className="p-4 text-left">Joined</th>
              <th className="p-4 text-left">Last Active</th>
              <th className="p-4 text-left">Status</th>
              {isAdmin && <th className="p-4 text-left">Actions</th>}
            </tr>
          </thead>
          <tbody>
            {members.map((member) => (
              <MemberTableRow
                key={member.id}
                member={member}
                isSelected={selectedMembers.has(member.userId)}
                onSelect={() => toggleSelect(member.userId)}
                showActions={isAdmin}
              />
            ))}
          </tbody>
        </table>
      </div>

      {/* Pagination */}
      <Pagination
        page={page}
        pageSize={20}
        total={totalCount}
        onPageChange={setPage}
      />
    </div>
  )
}