The organization router provides supplementary queries for organization (team) data. Core CRUD operations and team switching are handled by Better Auth’s organization plugin directly via authClient.organization.*.
This router provides:
- List user’s organizations with member counts
- Get active organization details
- Get organization members
- Update organization settings
- Delete organizations
Queries
listMyOrgs
List all organizations the current user is a member of.
Access: Protected
Response:
{
success: boolean;
orgs: Array<{
id: string;
name: string;
slug: string;
logo: string | null;
isPersonal: boolean;
createdAt: Date;
role: string; // User's role in this org
memberCount: number;
}>;
}
Example:
const { orgs } = await trpc.organization.listMyOrgs.query();
orgs.forEach(org => {
console.log(`${org.name} - ${org.memberCount} members - Role: ${org.role}`);
});
getActiveOrg
Get the active organization’s details with member count.
Access: Organization members only
Response:
{
success: boolean;
org: {
id: string;
name: string;
slug: string;
logo: string | null;
isPersonal: boolean;
createdAt: Date;
};
role: string; // Current user's role
memberCount: number;
}
The active organization is set via Better Auth’s organization plugin. Use authClient.organization.setActive() to switch organizations.
getMembers
Get all members of the active organization.
Access: Organization members only
Response:
{
success: boolean;
members: Array<{
id: string;
userId: string;
role: string;
createdAt: Date;
user: {
name: string | null;
image: string | null;
handle: string | null;
};
}>;
}
Example:
const { members } = await trpc.organization.getMembers.query();
members.forEach(member => {
console.log(`${member.user.name} - ${member.role}`);
});
Mutations
updateSlug
Update the organization’s slug (URL identifier).
Access: Organization owners only
New slug (2-32 characters, lowercase letters, numbers, and hyphens only)
Validation:
- Minimum 2 characters, maximum 32 characters
- Only lowercase letters, numbers, and hyphens
- Cannot start or end with a hyphen
- Cannot contain consecutive hyphens
- Cannot be a reserved slug
Example:
try {
const result = await trpc.organization.updateSlug.mutate({
slug: "my-awesome-team"
});
console.log(`Slug updated to: ${result.slug}`);
} catch (error) {
if (error.message.includes('reserved')) {
console.error('This slug is reserved');
} else if (error.message.includes('taken')) {
console.error('This slug is already in use');
}
}
deleteOrg
Delete the active organization.
Access: Organization owners only
Restrictions:
- Cannot delete personal teams
- Cannot delete organizations with multiple members (must remove members first)
Example:
try {
await trpc.organization.deleteOrg.mutate();
console.log('Organization deleted successfully');
} catch (error) {
if (error.message.includes('Personal teams')) {
console.error('Cannot delete your personal team');
} else if (error.message.includes('multiple members')) {
console.error('Remove all members before deleting');
}
}
Deleting an organization is permanent and will cascade to delete all related data including bounties, invitations, and member records.
Code Examples
Display Organization Switcher
import { trpc } from '@/lib/trpc';
import { authClient } from '@/lib/auth';
const OrganizationSwitcher = () => {
const { orgs } = trpc.organization.listMyOrgs.useQuery();
const { org: activeOrg } = trpc.organization.getActiveOrg.useQuery();
const switchOrg = async (orgId: string) => {
await authClient.organization.setActive({
organizationId: orgId
});
// Refresh the page or invalidate queries
window.location.reload();
};
return (
<div>
<h3>Current: {activeOrg?.org.name}</h3>
<ul>
{orgs?.orgs.map(org => (
<li key={org.id}>
<button onClick={() => switchOrg(org.id)}>
{org.name} ({org.memberCount} members)
</button>
</li>
))}
</ul>
</div>
);
};
Display Team Members
import { trpc } from '@/lib/trpc';
const TeamMembers = () => {
const { members } = trpc.organization.getMembers.useQuery();
return (
<div>
<h2>Team Members</h2>
{members?.members.map(member => (
<div key={member.id} className="member-card">
<img
src={member.user.image || '/default-avatar.png'}
alt={member.user.name || 'Member'}
/>
<div>
<h4>{member.user.name}</h4>
<p>@{member.user.handle}</p>
<span className="role-badge">{member.role}</span>
</div>
</div>
))}
</div>
);
};
Update Organization Slug
import { trpc } from '@/lib/trpc';
import { useState } from 'react';
const UpdateSlugForm = () => {
const [slug, setSlug] = useState('');
const updateMutation = trpc.organization.updateSlug.useMutation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await updateMutation.mutateAsync({ slug });
alert(`Slug updated to: ${result.slug}`);
} catch (error) {
alert(`Error: ${error.message}`);
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Organization Slug:
<input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value.toLowerCase())}
pattern="^[a-z0-9-]+$"
minLength={2}
maxLength={32}
required
/>
</label>
<button type="submit" disabled={updateMutation.isLoading}>
{updateMutation.isLoading ? 'Updating...' : 'Update Slug'}
</button>
</form>
);
};
Integration with Better Auth
The organization router works alongside Better Auth’s organization plugin. Here are the most common Better Auth operations:
import { authClient } from '@/lib/auth';
// Create a new organization
const newOrg = await authClient.organization.create({
name: "My Team",
slug: "my-team"
});
// Switch active organization
await authClient.organization.setActive({
organizationId: orgId
});
// Invite a member
await authClient.organization.inviteMember({
email: "[email protected]",
role: "member"
});
// Remove a member
await authClient.organization.removeMember({
membershipId: membershipId
});
// Update member role
await authClient.organization.updateMemberRole({
membershipId: membershipId,
role: "admin"
});
Use the tRPC organization router for read operations (listing, getting details) and Better Auth’s organization plugin for write operations (create, invite, remove members).