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.
| Property | Type | Description |
|---|---|---|
id | string | Unique membership ID |
userId | string | User ID of the member |
organizationId | string | Organization ID |
role | OrganizationRole | Member role in this organization |
joinedAt | Date | When the member joined |
invitedBy | string | null | User ID who invited this member |
status | "active" | "suspended" | Member status |
user | User | Associated user data |
user.id | string | User ID |
user.email | string | User email address |
user.name | string | null | User display name |
user.avatarUrl | string | null | User avatar URL |
user.lastActiveAt | Date | null | Last activity timestamp |
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.
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.
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
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.
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.
'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>
)
}