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, useCapxulIdentity } from "@capxul/sdk-react";
export function OrgList() {
const identity = useCapxulIdentity();
const orgs = useCapxulOrgs({ enabled: identity.phase === "authenticated" });
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"; seeds the initial role set. Creation supports exactly one shape. Any other value is refused withWRONG_STATE.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: "Solo",
});
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
CapxulOnboardingController 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 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.
There is no SDK hook for the switch, and there should not be: the switch is
navigation. useCapxulSwitchActingEntity used to sit here as a "stable seam",
but it carried no SDK side effect at all — it was a documented no-op — so
alpha.23 removed it. Navigate, and let the org hooks re-read for the new
orgId:
"use client";
import { useNavigate } from "react-router-dom";
function EntitySwitcher({ orgs }: { orgs: readonly OrgView[] }) {
const navigate = useNavigate();
return (
<select
onChange={(event) => {
const value = event.target.value;
navigate(value === "personal" ? "/dashboard" : `/orgs/${value}`);
}}
>
<option value="personal">Personal</option>
{orgs.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
);
}Read treasury, members, and roles
All three reads are parameterized by orgId. Gate them with options.enabled
until the canonical identity state contains that exact Organization in its
ready lane; another Organization or the personal Account must not unlock
the reads.
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 {
useCapxulAuth,
useCapxulIdentity,
useCapxulOrgMembers,
useCapxulOrgRoles,
useCapxulOrgTreasury,
useCapxulOrgs,
} from "@capxul/sdk-react";
import type { OrgId } from "@capxul/sdk";
export function OrgDetail({ orgId }: { orgId: OrgId }) {
const identity = useCapxulIdentity();
const auth = useCapxulAuth();
const orgs = useCapxulOrgs({ enabled: identity.phase === "authenticated" });
const org = orgs.data?.find((candidate) => candidate.id === orgId);
const lane =
identity.phase === "authenticated" && identity.account.at === "claimed"
? identity.account.org
: null;
const ready = lane?.at === "ready" && lane.orgId === orgId;
const treasury = useCapxulOrgTreasury(orgId, { enabled: ready });
const members = useCapxulOrgMembers(orgId, { enabled: ready });
const roles = useCapxulOrgRoles(orgId, { enabled: ready });
if (lane?.at === "failed") {
return lane.retryable ? (
<button onClick={() => void auth.retry()}>Retry organization setup</button>
) : (
<p>{lane.failure.message}</p>
);
}
if (!ready || orgs.isLoading || treasury.isPending || members.isPending) {
return <p>Loading organization…</p>;
}
if (orgs.isError) return <p>{orgs.error.message}</p>;
if (org == null) return <p>Organization not found.</p>;
return (
<>
<h2>{org.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 treasuryAccount: a pot of money withbalanceandavailable. Nothing else.useCapxulOrgMembers—MemberView[]:email,name,rolelabel, and a lifecyclestatusrunningpending(including the intermediatepending_safe/pending_grantsteps) →active, withrevoked/expiredoff-ramps. Apendingmember has been invited but has not finished joining yet.useCapxulOrgRoles—RoleView[]: each role'slabeland itsdefinition, 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.
Use Organization PermissionAssignment methods to assign or revoke current authority.
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.
Organization spending uses an explicit Permission through
client.org(orgId).payments or the matching React hooks.