Capxul Docs
Guides

Work with organizations

List the orgs a user belongs to, create one, switch the acting entity, read treasury/members/roles, and invite a member with a role.

This guide covers the organization surface end to end: list the organizations the signed-in user belongs to, create a new one, switch which entity the UI is acting as, read an org's treasury, members, and roles, and invite a member by email with a role.

Before you start

  • You have a working auth flow (Build the auth flow) and a signed-in, onboarded user.
  • Read Identity & orgs if you want the model behind entities, members, and roles — this guide only uses it.

Everything below is entity-scoped and explicit: the SDK holds no global "acting as" state. Every org read and write names its orgId, either as a hook argument or through client.org(orgId).

Render org screens behind your auth gate (bootstrap ready, session present). Most org hooks expect a bootstrapped provider and throw if rendered while the SDK is still starting up — the gate from Build the auth flow guarantees they never are.

List organizations

useCapxulOrgs lists the orgs the user belongs to. It is a session-scoped authenticated read — gate it on auth readiness so it does not fire before the session settles and surface a spurious NOT_AUTHENTICATED:

"use client";

import { useCapxulOrgs, useCapxulSession } from "@capxul/sdk-react";

export function OrgList() {
  const session = useCapxulSession();
  const orgs = useCapxulOrgs({ enabled: session.data != null });

  if (orgs.isLoading) return <p>Loading organizations…</p>;
  if (orgs.isError) return <p>{orgs.error.message}</p>;

  return (
    <ul>
      {(orgs.data ?? []).map((org) => (
        <li key={org.id}>
          <a href={`/orgs/${org.id}`}>{org.name}</a> · @{org.handle} · your role: {org.role}
        </li>
      ))}
    </ul>
  );
}

Each OrgView carries the org's id, name, handle, the viewing member's own role label within that org, and the org treasury snapshot.

Create an organization

useCapxulCreateOrg binds to client.createOrg(input). The input takes:

  • name — display name.
  • handle — globally unique, normalized slug (^[a-z0-9-]{3,32}$).
  • template"Solo", "Startup", or "Custom"; seeds the initial role set. For "Custom", pass your authored roles definitions.
  • country — optional.
"use client";

import { useCapxulCreateOrg } from "@capxul/sdk-react";

const createOrg = useCapxulCreateOrg();

const handleCreate = async () => {
  const org = await createOrg.mutateAsync({
    name: "Acme Studio",
    handle: "acme-studio",
    template: "Startup",
  });
  navigate(`/orgs/${org.id}`);
};

On success the hook invalidates the org list, so any mounted useCapxulOrgs refetches automatically.

If the user is onboarding as an organization founder for the first time, use useCapxulCompleteOrganizationOnboarding instead — it saves the founder's identity profile and creates the org in one step. useCapxulCreateOrg is for adding further orgs to an existing identity.

Switch the acting entity

"Acting as" (personal account vs. a specific org) is your app's state, not the SDK's. The idiomatic pattern — used by the reference app — is to derive the active entity from the route (/dashboard = personal, /orgs/:orgId = that org) and pass the derived orgId into the org hooks on those screens.

useCapxulSwitchActingEntity is the stable mutation seam for the switch interaction. It carries no SDK side effect — the actual context switch is whatever your app does afterwards (usually navigation):

"use client";

import { useNavigate } from "react-router-dom";
import { useCapxulOrgs, useCapxulSwitchActingEntity } from "@capxul/sdk-react";

export function EntitySwitcher({ activeOrgId }: { activeOrgId?: string }) {
  const navigate = useNavigate();
  const orgs = useCapxulOrgs();
  const switchEntity = useCapxulSwitchActingEntity();

  const handleChange = (value: string) => {
    if (value === "personal") {
      void switchEntity.mutateAsync(undefined).then(() => navigate("/dashboard"));
      return;
    }
    const org = orgs.data?.find((candidate) => candidate.id === value);
    if (org === undefined) return;
    void switchEntity.mutateAsync({ orgId: org.id }).then(() => navigate(`/orgs/${org.id}`));
  };

  return (
    <select value={activeOrgId ?? "personal"} onChange={(e) => handleChange(e.target.value)}>
      <option value="personal">Personal account</option>
      {orgs.data?.map((org) => (
        <option key={org.id} value={org.id}>
          {org.name}
        </option>
      ))}
    </select>
  );
}

Because scoping is explicit per call, two screens can act as two different entities at the same time without stepping on each other's cache — each org's data lives under its own ["capxul", "org", orgId, …] keys (see TanStack Query integration).

Read treasury, members, and roles

All three reads are parameterized by orgId and disabled until it is defined. OrgId is an opaque type you get from SDK data — OrgView.id from the org list, or org.id from a create/onboarding result. To turn a route param back into an OrgId, match it against the org list (orgs.data?.find((org) => org.id === param)?.id) rather than casting the raw string.

"use client";

import {
  useCapxulOrg,
  useCapxulOrgMembers,
  useCapxulOrgRoles,
  useCapxulOrgTreasury,
} from "@capxul/sdk-react";
import type { OrgId } from "@capxul/sdk";

export function OrgDetail({ orgId }: { orgId: OrgId }) {
  const org = useCapxulOrg(orgId);
  const treasury = useCapxulOrgTreasury(orgId);
  const members = useCapxulOrgMembers(orgId);
  const roles = useCapxulOrgRoles(orgId);

  if (org.isLoading || treasury.isLoading || members.isLoading) {
    return <p>Loading organization…</p>;
  }
  if (org.isError) return <p>{org.error.message}</p>;
  if (org.data == null) return <p>Organization not found.</p>;

  return (
    <>
      <h2>{org.data.name}</h2>
      {treasury.data ? (
        <p>
          Treasury: {treasury.data.balance.value} {treasury.data.balance.currency} (available:{" "}
          {treasury.data.available.value})
        </p>
      ) : null}
      <ul>
        {members.data?.map((member) => (
          <li key={`${member.orgId}:${member.email}`}>
            {member.email} · {member.role} · {member.status}
          </li>
        ))}
      </ul>
      <ul>
        {roles.data?.map((role) => (
          <li key={role.roleKey}>{role.label}</li>
        ))}
      </ul>
    </>
  );
}

What each returns:

  • useCapxulOrgTreasury — the org's single treasury Account: a pot of money with balance and available. Nothing else.
  • useCapxulOrgMembersMemberView[]: email, name, role label, and a lifecycle status running pending (including the intermediate pending_safe / pending_grant steps) → active, with revoked / expired off-ramps. A pending member has been invited but has not finished joining yet.
  • useCapxulOrgRolesRoleView[]: each role's label and its definition, including optional spend caps (perTx, perDay, allowed recipients) and management permissions. Role labels are what you pass when inviting or assigning.

Invite a member with a role

useCapxulInviteMember is scoped to an org and takes an email plus a role label (one of the labels from useCapxulOrgRoles):

"use client";

import { useCapxulInviteMember } from "@capxul/sdk-react";
import type { OrgId } from "@capxul/sdk";

export function InviteForm({ orgId }: { orgId: OrgId }) {
  const invite = useCapxulInviteMember(orgId);

  const handleInvite = async (email: string, role: string) => {
    const member = await invite.mutateAsync({ email, role });
    // member.status is "pending" until the invitee joins.
  };

  // …form UI…
}

Email is the universal entry point: inviting an address that is not yet a Capxul user still works — it creates a pending membership that resolves when that person signs in for the first time. On success the hook invalidates the org's member list.

To offboard or change someone's role later, use useCapxulRemoveMember and useCapxulAssignRole; both are scoped the same way and also refresh the member list.

What you end up with

  • An org list gated on auth, a create form that lands on the new org's page, and an entity switcher whose state lives in your router.
  • An org detail screen reading treasury, members, and roles for one explicit orgId.
  • An invite form that adds members by email with a role, including people who are not Capxul users yet.

Org spending (treasury payments, payroll) is a separate surface — see Capability status for what is buildable today.

On this page