Build a custom flow for creating and managing organization invitations
Organization members with appropriate permissions can invite new users to their organization and manage those invitations. The invitation recipient can be either an existing user of your application or a new user. If they are a new user, they will need to sign up in order to accept the invitation.
Users with the appropriate permissions can also revoke organization invitations for users that have not yet joined, which will prevent the user from becoming an organization member.
This guide will demonstrate how to use the Clerk API to build a custom flow for inviting users to an organization and managing an organization's pending invitations.
To invite a user:
- Use the
useOrganization()
hook to getorganization
, which is the active organization. - Use
organization
to call theinviteMember()
method, with the recipient's email address and desired role passed as arguments.
To revoke an invitation:
- Use the
useOrganization()
hook to getinvitations
, which is a list of invitations for the active organization. invitations
is an array ofOrganizationInvitation
objects. EachOrganizationInvitation
object has arevoke()
method that can be called to revoke the invitation.
The following example includes:
- An
<InviteMember />
component that allows administrators to invite new members to their organization. - An
<InvitationList />
component that lists all pending invitations and allows administrators to revoke them.
This example is written for Next.js App Router but can be adapted for any React-based framework.
'use client'
import { useOrganization } from '@clerk/nextjs'
import { OrganizationCustomRoleKey } from '@clerk/types'
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
export const OrgInvitationsParams = {
invitations: {
pageSize: 5,
keepPreviousData: true,
},
}
// Form to invite a new member to the organization.
export const InviteMember = () => {
const { isLoaded, organization, invitations } = useOrganization(OrgInvitationsParams)
const [emailAddress, setEmailAddress] = useState('')
const [disabled, setDisabled] = useState(false)
if (!isLoaded || !organization) {
return <>Loading</>
}
const onSubmit = async (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault()
const submittedData = Object.fromEntries(new FormData(e.currentTarget).entries()) as {
email: string | undefined
role: OrganizationCustomRoleKey | undefined
}
if (!submittedData.email || !submittedData.role) {
return
}
setDisabled(true)
await organization.inviteMember({
emailAddress: submittedData.email,
role: submittedData.role,
})
await invitations?.revalidate?.()
setEmailAddress('')
setDisabled(false)
}
return (
<form onSubmit={onSubmit}>
<input
name="email"
type="text"
placeholder="Email address"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
<label>Role</label>
<SelectRole fieldName={'role'} />
<button type="submit" disabled={disabled}>
Invite
</button>
</form>
)
}
type SelectRoleProps = {
fieldName?: string
isDisabled?: boolean
onChange?: ChangeEventHandler<HTMLSelectElement>
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { fieldName, isDisabled = false, onChange, defaultRole } = props
const { organization } = useOrganization()
const [fetchedRoles, setRoles] = useState<OrganizationCustomRoleKey[]>([])
const isPopulated = useRef(false)
useEffect(() => {
if (isPopulated.current) return
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRoles(res.data.map((roles) => roles.key as OrganizationCustomRoleKey))
})
}, [organization?.id])
if (fetchedRoles.length === 0) return null
return (
<select
name={fieldName}
disabled={isDisabled}
aria-disabled={isDisabled}
onChange={onChange}
defaultValue={defaultRole}
>
{fetchedRoles?.map((roleKey) => (
<option key={roleKey} value={roleKey}>
{roleKey}
</option>
))}
</select>
)
}
// List of pending invitations to an organization.
export const InvitationList = () => {
const { isLoaded, invitations, memberships } = useOrganization({
...OrgInvitationsParams,
...OrgMembersParams,
})
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Invited</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{invitations?.data?.map((inv) => (
<tr key={inv.id}>
<td>{inv.emailAddress}</td>
<td>{inv.createdAt.toLocaleDateString()}</td>
<td>{inv.role}</td>
<td>
<button
onClick={async () => {
await inv.revoke()
await Promise.all([memberships?.revalidate, invitations?.revalidate])
}}
>
Revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={!invitations?.hasPreviousPage || invitations?.isFetching}
onClick={() => invitations?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!invitations?.hasNextPage || invitations?.isFetching}
onClick={() => invitations?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
To check if the current user is an organization admin:
- Get the active organization's ID from the
clerk
object. - Call the
getOrganizationMemberships()
method to get a list of organizations that the user is a member of. This method returnsdata
, which is an array ofOrganizationMembership
objects. - In the list of organizations that the user is a member of, find the
OrganizationMembership
object that has an ID that matches the active organization's ID. - Check the
role
property of theOrganizationMembership
object to see if the user is an admin.
To invite a user:
- Use the active
organization
object to call theinviteMember()
method, with the recipient's email address and desired role passed as arguments.
To revoke an invitation:
- Use the active
organization
object to call thegetInvitations()
method to get an array ofOrganizationInvitation
objects. - Each
OrganizationInvitation
object has arevoke()
method that can be called to revoke the invitation.
The following example includes:
- A
renderInvitations()
function that lists all invitations and allows administrators to revoke them. - An
checkAdminAndRenderInvitations()
function that gets the current organization, checks if the current user is an admin, renders invitations, and sets up a form that allows administrators to invite new members to their organization.
Use the 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>
<h2>Invitations List</h2>
<ul id="invitations_list"></ul>
<h2>Send a new invitation</h2>
<form id="new_invitation">
<div>
<label>Email address</label>
<input type="email" id="email_address" name="email_address" />
</div>
<button>Invite</button>
</form>
<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.user) {
// Check for an active organization
if (clerk.organization) {
// Render list of organization invitations
async function renderInvitations(organization, isAdmin) {
const list = document.getElementById('invitations_list')
try {
const { data } = await organization.getInvitations()
const invitations = data
if (invitations.length === 0) {
list.textContent = 'No invitations'
}
invitations.map((invitation) => {
const li = document.createElement('li')
li.textContent = `${invitation.emailAddress} - ${invitation.role}`
// Add administrative actions; revoke invitation
if (isAdmin) {
const revokeBtn = document.createElement('button')
revokeBtn.textContent = 'Revoke'
revokeBtn.addEventListener('click', async function (e) {
e.preventDefault()
await invitation.revoke()
})
li.appendChild(revokeBtn)
}
// Add the entry to the list
list.appendChild(li)
})
} catch (err) {
console.error(err)
}
}
// Gets the current org, checks if the current user is an admin,
// renders invitations, and sets up the new invitation form.
async function checkAdminAndRenderInvitations() {
// This is the current organization ID.
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'
renderInvitations(currentOrganization, isAdmin)
if (isAdmin) {
const form = document.getElementById('new_invitation')
form.addEventListener('submit', async function (e) {
e.preventDefault()
const inputEl = document.getElementById('email_address')
if (!inputEl) return
try {
await currentOrganization.inviteMember({
emailAddress: inputEl.value,
role: 'org:member',
})
} catch (err) {
console.error(err)
}
})
}
}
checkAdminAndRenderInvitations()
} 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)
}
Next steps
Now that you've created a flow for managing organization invitations, you might want to create a flow for accepting invitations. See the dedicated custom flow guide for more information.
Feedback
Last updated on