Docs

Manage member roles in an organization

Organization members with appropriate permissions can manage member roles within an organization, including changing a member's role, removing them from an organization, and revoking organization invitations.

The useOrganization() hook can be used to retrieve the current user's membership or the list of memberships for the currently active organization. The destroy and update methods can be called on the membership object to remove a member or change their role.

The useOrganization() hook can also be used to retrieve the list of invitations for the currently active organization. The revoke method can be called on the invitation object to revoke the invitation.

Methods for changing a member's role, removing a member, and revoking an invitation are also available on the Backend API.

Usage

pages/organizations/[id].tsx
import { useState, useEffect, ChangeEventHandler, useRef } from "react"
import { useOrganization, useUser } from "@clerk/nextjs"
import type { OrganizationCustomRoleKey } from "@clerk/types"

export const OrgMembersParams = {
  memberships: {
    pageSize: 5,
    keepPreviousData: true,
  },
}

export const OrgInvitationsParams = {
  invitations: {
    pageSize: 5,
    keepPreviousData: true,
  },
}
export const OrgMembershipRequestsParams = {
  membershipRequests: {
    pageSize: 5,
    keepPreviousData: true,
  },
}

// View and manage active organization members,
// along with any pending invitations.
// Invite new members.
export default function Organization() {
  const {
    organization: currentOrganization,
    isLoaded,
  } = useOrganization();

  if (!isLoaded || !currentOrganization) {
    return null;
  }

  return (
    <>
      <h1>Organization: {currentOrganization.name}</h1>
      <h2 className="mb-6 mt-12">Custom List Invitations</h2>
      <OrgInvitations />
      <h2 className="mb-6 mt-12">Custom List Membership Requests</h2>
      <OrgMembershipRequests />
      <h2 className="mb-6 mt-12">Custom List Memberships</h2>
      <OrgMembers />
      <h2 className="mb-6 mt-12">Custom Invite Form</h2>
      <OrgInviteMemberForm />
    </>
  );
}

// List of organization memberships. Administrators can
// change member roles or remove members from the organization.
export const OrgMembers = () => {
  const { user } = useUser()
  const { isLoaded, memberships } = useOrganization(OrgMembersParams)

  if (!isLoaded) {
    return <>Loading</>
  }

  return (
    <>
      <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 className="flex">
        <button
          className="inline-block"
          disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
          onClick={() => memberships?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={!memberships?.hasNextPage || memberships?.isFetching}
          onClick={() => memberships?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

// List of pending invitations to an organization.
// You can invite new organization members and
// revoke already sent invitations.
export const OrgInvitations = () => {
  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 className="flex">
        <button
          className="inline-block"
          disabled={!invitations?.hasPreviousPage || invitations?.isFetching}
          onClick={() => invitations?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={!invitations?.hasNextPage || invitations?.isFetching}
          onClick={() => invitations?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

export const OrgMembershipRequests = () => {
  const { isLoaded, membershipRequests } = useOrganization(
    OrgMembershipRequestsParams
  )

  if (!isLoaded) {
    return <>Loading</>
  }

  return (
    <>
      <table>
        <thead>
          <tr>
            <th>User</th>
            <th>Requested Access</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {membershipRequests?.data?.map((mem) => (
            <tr key={mem.id}>
              <td>{mem.publicUserData.identifier}</td>
              <td>{mem.createdAt.toLocaleDateString()}</td>
              <td></td>
            </tr>
          ))}
        </tbody>
      </table>

      <div className="flex">
        <button
          className="inline-block"
          disabled={
            !membershipRequests?.hasPreviousPage ||
            membershipRequests?.isFetching
          }
          onClick={() => membershipRequests?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={
            !membershipRequests?.hasNextPage || membershipRequests?.isFetching
          }
          onClick={() => membershipRequests?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

export const OrgInviteMemberForm = () => {
  const { isLoaded, organization, invitations } = useOrganization(OrgInvitationsParams)
  const [emailAddress, setEmailAddress] = useState("")
  const [disabled, setDisabled] = useState(false)

  if (!isLoaded || !organization) {
    return <>Loading</>
  }

  const onSubmit = async (e) => {
    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>
  )
}
import { useState, useEffect, ChangeEventHandler, useRef } from "react"
import { useOrganization, useUser } from "@clerk/clerk-react"
import type { OrganizationCustomRoleKey } from "@clerk/types"

export const OrgMembersParams = {
  memberships: {
    pageSize: 5,
    keepPreviousData: true,
  },
}

export const OrgInvitationsParams = {
  invitations: {
    pageSize: 5,
    keepPreviousData: true,
  },
}
export const OrgMembershipRequestsParams = {
  membershipRequests: {
    pageSize: 5,
    keepPreviousData: true,
  },
}

// View and manage active organization members,
// along with any pending invitations.
// Invite new members.
export default function Organization() {
  const {
    organization: currentOrganization,
    isLoaded,
  } = useOrganization();

  if (!isLoaded || !currentOrganization) {
    return null;
  }

  return (
    <>
      <h1>Organization: {currentOrganization.name}</h1>
      <h2 className="mb-6 mt-12">Custom List Invitations</h2>
      <OrgInvitations />
      <h2 className="mb-6 mt-12">Custom List Membership Requests</h2>
      <OrgMembershipRequests />
      <h2 className="mb-6 mt-12">Custom List Memberships</h2>
      <OrgMembers />
      <h2 className="mb-6 mt-12">Custom Invite Form</h2>
      <OrgInviteMemberForm />
    </>
  );
}

// List of organization memberships. Administrators can
// change member roles or remove members from the organization.
export const OrgMembers = () => {
  const { user } = useUser()
  const { isLoaded, memberships } = useOrganization(OrgMembersParams)

  if (!isLoaded) {
    return <>Loading</>
  }

  return (
    <>
      <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 className="flex">
        <button
          className="inline-block"
          disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
          onClick={() => memberships?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={!memberships?.hasNextPage || memberships?.isFetching}
          onClick={() => memberships?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

// List of pending invitations to an organization.
// You can invite new organization members and
// revoke already sent invitations.
export const OrgInvitations = () => {
  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 className="flex">
        <button
          className="inline-block"
          disabled={!invitations?.hasPreviousPage || invitations?.isFetching}
          onClick={() => invitations?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={!invitations?.hasNextPage || invitations?.isFetching}
          onClick={() => invitations?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

export const OrgMembershipRequests = () => {
  const { isLoaded, membershipRequests } = useOrganization(
    OrgMembershipRequestsParams
  )

  if (!isLoaded) {
    return <>Loading</>
  }

  return (
    <>
      <table>
        <thead>
          <tr>
            <th>User</th>
            <th>Requested Access</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {membershipRequests?.data?.map((mem) => (
            <tr key={mem.id}>
              <td>{mem.publicUserData.identifier}</td>
              <td>{mem.createdAt.toLocaleDateString()}</td>
              <td></td>
            </tr>
          ))}
        </tbody>
      </table>

      <div className="flex">
        <button
          className="inline-block"
          disabled={
            !membershipRequests?.hasPreviousPage ||
            membershipRequests?.isFetching
          }
          onClick={() => membershipRequests?.fetchPrevious?.()}
        >
          Previous
        </button>

        <button
          className="inline-block"
          disabled={
            !membershipRequests?.hasNextPage || membershipRequests?.isFetching
          }
          onClick={() => membershipRequests?.fetchNext?.()}
        >
          Next
        </button>
      </div>
    </>
  )
}

export const OrgInviteMemberForm = () => {
  const { isLoaded, organization, invitations } = useOrganization(OrgInvitationsParams)
  const [emailAddress, setEmailAddress] = useState("")
  const [disabled, setDisabled] = useState(false)

  if (!isLoaded || !organization) {
    return <>Loading</>
  }

  const onSubmit = async (e) => {
    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>
  )
}
<p>List of memberships:</p>
<ul id="memberships_list"></ul>

<p>List of invitations:</p>
<ul id="invitations_list"></ul>

<p>Send a new invitation:</p>
<form id="new_invitation">
  <div>
    <label>Email address</div>
    <br />
    <input type="email" id="email_address" name="email_address" />
  </div>
  <button>Invite</button>
</form>

<script>
  async function renderMemberships(organization, isAdmin) {
    const list = document.getElementById("memberships_list");
    try {
      const { data: memberships } = await organization.getMemberships();

      memberships.map((membership) => {
        const li = document.createElement("li");
        li.textContent = `${membership.publicUserData.identifier} - ${membership.role}`;

        // Add administrative actions; update role and remove member.
        if (isAdmin) {
          const updateBtn = document.createElement("button");
          updateBtn.textContent = "Change role";
          updateBtn.addEventListener("click", async function(e) {
            e.preventDefault();
            const role = membership.role === "admin" ?
              "org:member" :
              "admin";
            await membership.update({ role });
          });
          li.appendChild(updateBtn);

          const removeBtn = document.createElement("button");
          removeBtn.textContent = "Remove";
          removeBtn.addEventListener("click", async function(e) {
            e.preventDefault();
            await currentOrganization.removeMember(membership.userId);
          });
          li.appendChild(removeBtn);
        }

        // Add the entry to the list
        list.appendChild(li);
      });
    } catch (err) {
      console.error(err);
    }
  }

  async function renderInvitations(organization, isAdmin) {
    const list = document.getElementById("invitations_list");
    try {
      const { data: invitations } = await organization.getInvitations();

      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);
    }
  }

  async function init() {
    // This is the current organization ID.
    const organizationId = "org_XXXXXXX";
    const { data: organizationMemberships } = await window.Clerk.user.getOrganizationMemberships();

    const currentMembership = organizationMemberships.find(membership
      => membership.organization.id === organizationId);
    const currentOrganization = currentMembership.organization;

    if (!currentOrganization) {
      return;
    }
    const isAdmin = currentMembership.role === "admin";

    renderMemberships(currentOrganization, isAdmin);
    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);
        }
      });
    }
  }

  init();
</script>

Feedback

What did you think of this content?