Build a custom flow for managing member Roles in an Organization
Organization members with appropriate Permissions can manage a member's Roles and remove members within an Organization.
This guide will demonstrate how to use the Clerk API to build a custom flow for managing member Roles in an Organization.
The following example:
- Uses the useOrganization() hook to get
memberships, which is a list of the memberships.membershipsis an object withdatathat contains an array ofOrganizationMembershipobjects.- Each
OrganizationMembershipobject has anupdate()anddestroy()method to update the member's Role and remove the member from the Organization, respectively.
- Maps over the
dataarray to display the memberships in a table, providing an "Update Role" and "Remove Member" button for each membership that calls theupdate()anddestroy()methods, respectively.
'use client'
import { useState, useEffect, ChangeEventHandler, useRef } from 'react'
import { useOrganization, useUser } from '@clerk/nextjs'
import type { OrganizationCustomRoleKey } from '@clerk/shared/types'
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization memberships. Administrators can
// change member Roles or remove members from the Organization.
export const ManageRoles = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<h1>Memberships List</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Joined</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{memberships?.data?.map((mem) => (
<tr key={mem.id}>
<td>
{mem.publicUserData.identifier} {mem.publicUserData.userId === user?.id && '(You)'}
</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>
<SelectRole
defaultRole={mem.role}
onChange={async (e) => {
await mem.update({
role: e.target.value as OrganizationCustomRoleKey,
})
await memberships?.revalidate()
}}
/>
</td>
<td>
<button
onClick={async () => {
await mem.destroy()
await memberships?.revalidate()
}}
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onClick={() => memberships?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onClick={() => memberships?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
type SelectRoleProps = {
fieldName?: string
onChange?: ChangeEventHandler<HTMLSelectElement>
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { fieldName, onChange, defaultRole } = props
const { organization } = useOrganization()
const [rolesResponse, setRolesResponse] =
useState < ReturnType < NonNullable < typeof organization > ['getRoles'] >> [0] > []
const isPopulated = useRef(false)
useEffect(() => {
if (isPopulated.current) return
setIsLoading(true)
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRolesResponse(res)
})
}, [organization?.id])
if (rolesResponse.data?.length === 0) return null
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const isDisabled = !!rolesResponse.has_role_set_migration
const roleKeys = rolesResponse.data.map((role) => role.key)
return (
<select
name={fieldName}
disabled={isDisabled}
aria-disabled={isDisabled}
onChange={onChange}
defaultValue={defaultRole}
>
{roleKeys?.map((roleKey) => (
<option key={roleKey} value={roleKey}>
{roleKey}
</option>
))}
</select>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useOrganization, useUser } from '@clerk/expo'
import type { OrganizationCustomRoleKey } from '@clerk/shared/types'
import * as React from 'react'
import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, View } from 'react-native'
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization memberships. Administrators can
// change member Roles or remove members from the Organization.
export const ManageRoles = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" />
<ThemedText>Loading</ThemedText>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Memberships List
</ThemedText>
{memberships?.data && memberships.data.length > 0 ? (
<>
<ScrollView style={styles.scrollView}>
{memberships?.data?.map((mem) => (
<View key={mem.id} style={styles.card}>
<ThemedText style={styles.label}>User:</ThemedText>
<ThemedText style={styles.value}>
{mem.publicUserData?.identifier}{' '}
{mem.publicUserData?.userId === user?.id && '(You)'}
</ThemedText>
<ThemedText style={styles.label}>Joined:</ThemedText>
<ThemedText style={styles.value}>{mem.createdAt.toLocaleDateString()}</ThemedText>
<ThemedText style={styles.label}>Role:</ThemedText>
<SelectRole
defaultRole={mem.role}
onRoleSelect={async (role) => {
await mem.update({
role: role,
})
await memberships?.revalidate()
}}
/>
<Pressable
style={({ pressed }) => [
styles.button,
styles.removeButton,
pressed && styles.buttonPressed,
]}
onPress={async () => {
await mem.destroy()
await memberships?.revalidate()
}}
>
<ThemedText style={styles.buttonText}>Remove</ThemedText>
</Pressable>
</View>
))}
</ScrollView>
<View style={styles.pagination}>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!memberships?.hasPreviousPage || memberships?.isFetching) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onPress={() => memberships?.fetchPrevious?.()}
>
<ThemedText style={styles.buttonText}>Previous</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!memberships?.hasNextPage || memberships?.isFetching) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onPress={() => memberships?.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Next</ThemedText>
</Pressable>
</View>
</>
) : (
<ThemedText>No memberships found</ThemedText>
)}
</ThemedView>
)
}
type SelectRoleProps = {
onRoleSelect?: (role: OrganizationCustomRoleKey) => void
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { onRoleSelect, defaultRole } = props
const { organization } = useOrganization()
const [rolesResponse, setRolesResponse] =
React.useState < ReturnType < NonNullable < typeof organization > ['getRoles'] >> [0] > []
const [showModal, setShowModal] = React.useState(false)
const [selectedRole, setSelectedRole] = React.useState<OrganizationCustomRoleKey | undefined>(
defaultRole as OrganizationCustomRoleKey | undefined,
)
const isPopulated = React.useRef(false)
React.useEffect(() => {
if (isPopulated.current) return
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRolesResponse(res)
})
}, [organization?.id])
React.useEffect(() => {
setSelectedRole(defaultRole as OrganizationCustomRoleKey | undefined)
}, [defaultRole])
if (rolesResponse.data?.length === 0) return null
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const isDisabled = !!rolesResponse.has_role_set_migration
const roleKeys = rolesResponse.data.map((role) => role.key)
const handleRoleSelect = (role: OrganizationCustomRoleKey) => {
setSelectedRole(role)
setShowModal(false)
onRoleSelect?.(role)
}
return (
<View>
<Pressable
style={({ pressed }) => [
styles.selectButton,
isDisabled && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={() => !isDisabled && setShowModal(true)}
disabled={isDisabled}
>
<ThemedText style={styles.selectButtonText}>{selectedRole || 'Select role'}</ThemedText>
</Pressable>
<Modal visible={showModal} transparent animationType="slide">
<ThemedView style={styles.modalOverlay}>
<ThemedView style={styles.modalContent}>
<ThemedText type="title" style={styles.modalTitle}>
Select Role
</ThemedText>
<ScrollView>
{roleKeys?.map((roleKey) => (
<Pressable
key={roleKey}
style={({ pressed }) => [
styles.roleOption,
selectedRole === roleKey && styles.roleOptionSelected,
pressed && styles.buttonPressed,
]}
onPress={() => handleRoleSelect(roleKey)}
>
<ThemedText
style={[
styles.roleOptionText,
selectedRole === roleKey && styles.roleOptionTextSelected,
]}
>
{roleKey}
</ThemedText>
</Pressable>
))}
</ScrollView>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={() => setShowModal(false)}
>
<ThemedText style={styles.buttonText}>Cancel</ThemedText>
</Pressable>
</ThemedView>
</ThemedView>
</Modal>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
title: {
marginBottom: 20,
},
label: {
fontWeight: '600',
fontSize: 14,
},
value: {
fontSize: 16,
marginBottom: 8,
},
scrollView: {
flex: 1,
},
card: {
backgroundColor: 'rgba(128, 128, 128, 0.1)',
borderRadius: 8,
padding: 16,
marginBottom: 12,
gap: 8,
},
selectButton: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
backgroundColor: '#fff',
marginBottom: 8,
},
selectButtonText: {
fontSize: 16,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
removeButton: {
backgroundColor: '#dc3545',
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
pagination: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
gap: 12,
},
paginationButton: {
flex: 1,
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContent: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 20,
width: '80%',
maxHeight: '70%',
gap: 16,
},
modalTitle: {
marginBottom: 16,
},
roleOption: {
padding: 12,
borderRadius: 8,
marginBottom: 8,
backgroundColor: '#f0f0f0',
},
roleOptionSelected: {
backgroundColor: '#0a7ea4',
},
roleOptionText: {
fontSize: 16,
},
roleOptionTextSelected: {
color: '#fff',
fontWeight: '600',
},
})The following example includes a checkAdminAndRenderMemberships() function that checks if the user is an admin of the currently active Organization and calls renderMemberships(). The renderMemberships() function lists the Organization's memberships and allows administrators to update a member's Role and remove a member from the Organization.
Use the following tabs to view the code necessary for the index.html and main.js files.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clerk + JavaScript App</title>
</head>
<body>
<div id="app"></div>
<h1>Memberships List</h1>
<table>
<thead>
<tr>
<th>User ID</th>
<th>User identifier</th>
<th>Joined</th>
<th>Role</th>
<th id="update-role-head" hidden>Update role</th>
<th id="remove-member-head" hidden>Remove member</th>
</tr>
</thead>
<tbody id="memberships-table-body"></tbody>
</table>
<script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
</body>
</html>import { Clerk } from '@clerk/clerk-js'
const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!pubKey) {
throw new Error('Add your VITE_CLERK_PUBLISHABLE_KEY to .env file')
}
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY')
await clerk.load()
if (clerk.isSignedIn) {
// Check for an Active Organization
if (clerk.organization) {
// Render list of organization memberships
async function renderMemberships(organization, isAdmin) {
try {
const { data } = await organization.getMemberships()
const memberships = data
console.log(`getMemberships:`, memberships)
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const rolesResponse = await organization.getRoles()
const isRolesMigrationDisabled = rolesResponse.has_role_set_migration
memberships.map((membership) => {
const membershipTable = document.getElementById('memberships-table-body')
const row = membershipTable.insertRow()
row.insertCell().textContent = membership.publicUserData.userId
row.insertCell().textContent = membership.publicUserData.identifier
row.insertCell().textContent = membership.createdAt.toLocaleDateString()
row.insertCell().textContent = membership.role
// Add administrative actions:
// Add and remove a member, and update a member's Role.
if (isAdmin) {
// Show update and remove member buttons
document.getElementById('update-role-head').removeAttribute('hidden')
document.getElementById('remove-member-head').removeAttribute('hidden')
// Get the user ID of the member
const userId = membership.publicUserData.userId
// Update a member's Role
const updateBtn = document.createElement('button')
updateBtn.textContent = 'Change role'
updateBtn.disabled = isRolesMigrationDisabled
updateBtn.addEventListener('click', async function (e) {
e.preventDefault()
if (isRolesMigrationDisabled) {
return
}
const role = membership.role === 'org:admin' ? 'org:member' : 'org:admin'
await organization
.updateMember({ userId, role })
.then((res) => console.log(`updateMember response:`, res))
.catch((error) => console.log('An error occurred:', error))
})
row.insertCell().appendChild(updateBtn)
// Remove a member
const removeBtn = document.createElement('button')
removeBtn.textContent = 'Remove'
removeBtn.addEventListener('click', async function (e) {
e.preventDefault()
await organization
.removeMember(userId)
.then((res) => console.log(`removeMember response:`, res))
.catch((error) => console.log('An error occurred:', error))
})
row.insertCell().appendChild(removeBtn)
}
})
} catch (error) {
console.log('An error occurred:', error)
}
}
/**
* Checks if a user is an admin of the
* currently Active Organization and
* renders the Organization's memberships.
*/
async function checkAdminAndRenderMemberships() {
const organizationId = clerk.organization.id
const { data } = await clerk.user.getOrganizationMemberships()
const organizationMemberships = data
const currentMembership = organizationMemberships.find(
(membership) => membership.organization.id === organizationId,
)
const currentOrganization = currentMembership.organization
if (!currentOrganization) return
const isAdmin = currentMembership.role === 'org:admin'
console.log(`Current organization:`, currentOrganization)
renderMemberships(currentOrganization, isAdmin)
}
checkAdminAndRenderMemberships()
} else {
// If there is no Active Organization,
// mount Clerk's <OrganizationSwitcher />
// to allow the user to set an Organization as active
document.getElementById('app').innerHTML = `
<h2>Select an Organization to set it as active</h2>
<div id="org-switcher"></div>
`
const orgSwitcherDiv = document.getElementById('org-switcher')
clerk.mountOrganizationSwitcher(orgSwitcherDiv)
}
} else {
// If there is no active user, mount Clerk's <SignIn />
document.getElementById('app').innerHTML = `
<div id="sign-in"></div>
`
const signInDiv = document.getElementById('sign-in')
clerk.mountSignIn(signInDiv)
}import SwiftUI
import ClerkKit
struct ManageRolesView: View {
@State private var memberships: [OrganizationMembership] = []
let organization: Organization
var body: some View {
VStack {
ForEach(memberships) { membership in
HStack {
Text("\(membership.publicUserData?.identifier ?? "Unknown user"): \(membership.role)")
Button("Toggle role") {
Task {
let role = membership.role == "org:admin" ? "org:member" : "org:admin"
await updateMembershipRole(membership, role: role)
await fetchOrganizationMemberships()
}
}
Button("Remove") {
Task {
await removeMembership(membership)
await fetchOrganizationMemberships()
}
}
}
}
}
.task { await fetchOrganizationMemberships() }
}
}
extension ManageRolesView {
func fetchOrganizationMemberships() async {
do {
memberships = try await organization.getMemberships().data
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
memberships = []
}
}
func updateMembershipRole(_ membership: OrganizationMembership, role: String) async {
do {
try await membership.update(role: role)
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
func removeMembership(_ membership: OrganizationMembership) async {
do {
try await membership.destroy()
} catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
dump(error)
}
}
}import com.clerk.api.Clerk
import com.clerk.api.network.serialization.ClerkResult
import com.clerk.api.organizations.OrganizationMembership
import com.clerk.api.organizations.getOrganizationMemberships
import com.clerk.api.organizations.removeMember
import com.clerk.api.organizations.updateMembership
import com.clerk.api.user.getOrganizationMemberships
suspend fun fetchOrganizationMemberships(): List<OrganizationMembership> {
val activeOrganizationId = Clerk.session?.lastActiveOrganizationId ?: return emptyList()
val membershipsResult = Clerk.user?.getOrganizationMemberships() ?: return emptyList()
if (membershipsResult !is ClerkResult.Success) return emptyList()
val activeOrganization =
membershipsResult.value.data.firstOrNull { it.organization.id == activeOrganizationId }?.organization
?: return emptyList()
return when (val activeMemberships = activeOrganization.getOrganizationMemberships()) {
is ClerkResult.Success -> activeMemberships.value.data
is ClerkResult.Failure -> emptyList()
}
}
suspend fun updateMembershipRole(membership: OrganizationMembership, role: String) {
val userId = membership.publicUserData?.userId ?: return
membership.updateMembership(userId = userId, role = role)
}
suspend fun removeMembership(membership: OrganizationMembership) {
val userId = membership.publicUserData?.userId ?: return
membership.organization.removeMember(userId)
}Feedback
Last updated on