Organizations
Multi-tenant organization features with Better Auth and Convex.
In this guide, we'll explore multi-tenant organization features with better-convex. You'll learn to set up the organization plugin, define roles and permissions, manage members and invitations, and implement team functionality.
Overview
Multi-tenant organization system with Better Auth's organization plugin:
| Feature | Description |
|---|---|
| Organizations | Create, update, delete workspaces |
| Members | Role-based access within orgs |
| Invitations | Email invites with expiration |
| Teams | Group members within orgs |
| Access control | Custom permissions per role |
| Hooks | Lifecycle events for all operations |
Let's set up organizations step by step.
Prerequisites
Ensure you have Auth Server set up before adding organizations.
1. Server Configuration
Add the organization plugin to your auth options:
import { organization } from 'better-auth/plugins';
import type { ActionCtx } from './_generated/server';
export const createAuthOptions = (ctx: GenericCtx) =>
({
// ... existing config
plugins: [
convex({ authConfig, jwks: process.env.JWKS }),
admin(),
organization({
ac,
roles,
allowUserToCreateOrganization: true,
organizationLimit: 5,
membershipLimit: 100,
creatorRole: 'owner',
invitationExpiresIn: 48 * 60 * 60, // 48 hours
teams: {
enabled: true,
maximumTeams: 10,
},
sendInvitationEmail: async (data) => {
await (ctx as ActionCtx).scheduler.runAfter(
0,
internal.email.sendOrganizationInviteEmail,
{
acceptUrl: `${process.env.SITE_URL!}/w/${data.organization.slug}?invite=${data.id}`,
invitationId: data.id,
inviterEmail: data.inviter.user.email,
inviterName: data.inviter.user.name || 'Team Admin',
organizationName: data.organization.name,
role: data.role,
to: data.email,
}
);
},
}),
],
}) satisfies BetterAuthOptions;2. Client Configuration
Add the organization client plugin:
import { organizationClient } from 'better-auth/client/plugins';
import { ac, roles } from '@convex/auth-shared';
export const authClient = createAuthClient({
// ... existing config
plugins: [
inferAdditionalFields<Auth>(),
convexClient(),
organizationClient({
ac,
roles,
teams: { enabled: true },
}),
],
});3. Schema
Add organization tables to your schema:
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
// ... auth tables
organization: defineTable({
name: v.string(),
slug: v.string(),
logo: v.optional(v.string()),
createdAt: v.number(),
metadata: v.optional(v.string()),
})
.index('slug', ['slug'])
.index('name', ['name']),
member: defineTable({
organizationId: v.id('organization'),
userId: v.id('user'),
role: v.string(),
createdAt: v.number(),
})
.index('userId', ['userId'])
.index('organizationId_userId', ['organizationId', 'userId'])
.index('organizationId_role', ['organizationId', 'role']),
invitation: defineTable({
organizationId: v.id('organization'),
inviterId: v.id('user'),
email: v.string(),
role: v.optional(v.string()),
status: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
})
.index('email', ['email'])
.index('status', ['status'])
.index('email_organizationId_status', ['email', 'organizationId', 'status'])
.index('organizationId_status', ['organizationId', 'status']),
// Session needs activeOrganizationId
session: defineTable({
// ... existing session fields
activeOrganizationId: v.optional(v.string()),
activeTeamId: v.optional(v.string()),
}),
});import { v } from 'convex/values';
import { defineEnt, defineEntSchema } from 'convex-ents';
const schema = defineEntSchema({
// ... auth tables
user: defineEnt({
// ... existing fields
})
.edges('members', { to: 'member', ref: 'userId' })
.edges('invitations', { to: 'invitation', ref: 'inviterId' }),
organization: defineEnt({
logo: v.optional(v.string()),
createdAt: v.number(),
metadata: v.optional(v.string()),
})
.field('slug', v.string(), { unique: true })
.field('name', v.string(), { index: true })
.edges('members', { to: 'member', ref: true })
.edges('invitations', { to: 'invitation', ref: true }),
member: defineEnt({
createdAt: v.number(),
})
.field('role', v.string(), { index: true })
.edge('organization', { to: 'organization', field: 'organizationId' })
.edge('user', { to: 'user', field: 'userId' })
.index('organizationId_userId', ['organizationId', 'userId'])
.index('organizationId_role', ['organizationId', 'role']),
invitation: defineEnt({
role: v.optional(v.string()),
expiresAt: v.number(),
createdAt: v.number(),
})
.field('email', v.string(), { index: true })
.field('status', v.string(), { index: true })
.edge('organization', { to: 'organization', field: 'organizationId' })
.edge('inviter', { to: 'user', field: 'inviterId' })
.index('email_organizationId_status', ['email', 'organizationId', 'status'])
.index('organizationId_status', ['organizationId', 'status']),
// Session needs activeOrganizationId
session: defineEnt({
// ... existing session fields
activeOrganizationId: v.optional(v.string()),
activeTeamId: v.optional(v.string()),
}),
});Teams (Optional)
team: defineTable({
name: v.string(),
organizationId: v.id('organization'),
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
.index('organizationId', ['organizationId']),
teamMember: defineTable({
teamId: v.id('team'),
userId: v.id('user'),
createdAt: v.optional(v.number()),
})
.index('teamId', ['teamId'])
.index('userId', ['userId']),team: defineEnt({
name: v.string(),
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
.edge('organization', { to: 'organization', field: 'organizationId' })
.index('organizationId', ['organizationId']),
teamMember: defineEnt({
createdAt: v.optional(v.number()),
})
.edge('team', { to: 'team', field: 'teamId' })
.edge('user', { to: 'user', field: 'userId' })
.index('userId', ['userId']),Additional Fields
Add custom fields to organization tables:
organization({
schema: {
organization: {
fields: {
description: v.optional(v.string()),
website: v.optional(v.string()),
industry: v.optional(v.string()),
size: v.optional(v.string()),
},
},
member: {
fields: {
title: v.optional(v.string()),
department: v.optional(v.string()),
},
},
invitation: {
fields: {
message: v.optional(v.string()),
},
},
},
}),Then update your schema to include these fields:
organization: defineTable({
// ... existing fields
description: v.optional(v.string()),
website: v.optional(v.string()),
industry: v.optional(v.string()),
size: v.optional(v.string()),
}),
member: defineTable({
// ... existing fields
title: v.optional(v.string()),
department: v.optional(v.string()),
}),organization: defineEnt({
// ... existing fields
description: v.optional(v.string()),
website: v.optional(v.string()),
industry: v.optional(v.string()),
size: v.optional(v.string()),
}),
member: defineEnt({
// ... existing fields
title: v.optional(v.string()),
department: v.optional(v.string()),
}),4. Access Control
Define roles and permissions using Better Auth's access control system.
Basic Setup
import { createAccessControl } from 'better-auth/plugins/access';
import {
defaultStatements,
memberAc,
ownerAc,
} from 'better-auth/plugins/organization/access';
const statement = {
...defaultStatements,
// Add custom resources as needed
} as const;
export const ac = createAccessControl(statement);
const member = ac.newRole({
...memberAc.statements,
});
const owner = ac.newRole({
...ownerAc.statements,
});
export const roles = { member, owner };Custom Permissions
Define custom resources and permissions beyond the defaults:
const statement = {
...defaultStatements,
// Custom resources
project: ['create', 'read', 'update', 'delete'],
billing: ['read', 'update'],
analytics: ['read'],
} as const;
export const ac = createAccessControl(statement);
// Custom roles with granular permissions
const viewer = ac.newRole({
project: ['read'],
analytics: ['read'],
});
const editor = ac.newRole({
...memberAc.statements,
project: ['create', 'read', 'update'],
analytics: ['read'],
});
const admin = ac.newRole({
...ownerAc.statements,
project: ['create', 'read', 'update', 'delete'],
billing: ['read', 'update'],
analytics: ['read'],
});
export const roles = { viewer, editor, admin };Check Role Permissions
// Check if a role has specific permission
const canEdit = ac.checkRolePermission({
role: 'editor',
permission: { project: ['update'] },
});Dynamic Access Control
For runtime role creation (e.g., user-defined roles):
organization({
ac: {
...ac,
resolveRole: async ({ role, organizationId }) => {
// Check if it's a built-in role
if (roles[role]) return roles[role];
// Fetch custom role from database
const customRole = await ctx.table('customRole')
.filter((q) => q.eq(q.field('name'), role))
.first();
if (customRole) {
return ac.newRole(customRole.permissions);
}
return null;
},
},
}),Permission Helper
import { CRPCError } from 'better-convex/server';
import type { AuthCtx } from '../crpc';
export const hasPermission = async (
ctx: AuthCtx,
body: { permissions: Record<string, string[]> },
shouldThrow = true
) => {
const result = await ctx.auth.api.hasPermission({
body,
headers: ctx.auth.headers,
});
if (shouldThrow && !result.success) {
throw new CRPCError({
code: 'FORBIDDEN',
message: 'Insufficient permissions',
});
}
return result.success;
};Check Permission (Client)
// Check permission before showing UI
const { data } = await authClient.organization.hasPermission({
permissions: { project: ['delete'] },
});
if (data?.success) {
// Show delete button
}Organization Functions
Pattern: Use Better Auth API for multi-table operations (create, delete, invitations). Use direct table operations for simple reads/updates.
Function examples use ctx.table (Convex Ents). For ctx.db, replace edge traversal with manual joins. See Ents for migration patterns.
Check Slug Availability
Before creating an organization, validate the slug is unique:
export const checkSlug = authQuery
.input(z.object({ slug: z.string() }))
.output(z.object({ available: z.boolean() }))
.query(async ({ ctx, input }) => {
const existing = await ctx.table('organization').get('slug', input.slug);
return { available: !existing };
});const { data } = await authClient.organization.checkSlug({ slug: 'my-org' });
if (!data.available) {
// Show error: slug already taken
}List Organizations
import { z } from 'zod';
import { zid } from 'convex-helpers/server/zod';
import { asyncMap } from 'convex-helpers';
import { authQuery } from '../lib/crpc';
export const list = authQuery
.output(
z.object({
organizations: z.array(
z.object({
id: zid('organization'),
name: z.string(),
slug: z.string(),
logo: z.string().nullish(),
isPersonal: z.boolean(),
})
),
})
)
.query(async ({ ctx }) => {
const members = await ctx.table('member', 'userId', (q) =>
q.eq('userId', ctx.userId)
);
const orgs = await asyncMap(members, async (member) => {
const org = await member.edgeX('organization');
return {
id: org._id,
name: org.name,
slug: org.slug,
logo: org.logo,
isPersonal: org._id === ctx.user.personalOrganizationId,
};
});
return { organizations: orgs };
});Create Organization
export const create = authMutation
.input(
z.object({
name: z.string().min(1).max(100),
})
)
.output(
z.object({
id: zid('organization'),
slug: z.string(),
})
)
.mutation(async ({ ctx, input }) => {
const slug = generateSlug(input.name);
// Use Better Auth API (handles org + member creation)
const org = await ctx.auth.api.createOrganization({
body: { name: input.name, slug },
headers: ctx.auth.headers,
});
if (!org) {
throw new CRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create organization',
});
}
// Set as active
await ctx.auth.api.setActiveOrganization({
body: { organizationId: org.id },
headers: ctx.auth.headers,
});
return { id: org.id, slug: org.slug };
});Update Organization
// Direct table patch (simpler for single-table updates)
export const update = authMutation
.input(
z.object({
name: z.string().min(1).max(100).optional(),
logo: z.string().url().optional(),
slug: z.string().optional(),
})
)
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { organization: ['update'] } });
await ctx
.table('organization')
.getX(ctx.user.activeOrganization!.id)
.patch({
name: input.name,
logo: input.logo,
...(input.slug ? { slug: input.slug } : {}),
});
return null;
});Delete Organization
// Use Better Auth API (handles cleanup)
export const remove = authMutation
.output(z.null())
.mutation(async ({ ctx }) => {
await hasPermission(ctx, { permissions: { organization: ['delete'] } });
const organizationId = ctx.user.activeOrganization!.id;
// Prevent deletion of personal organizations
if (organizationId === ctx.user.personalOrganizationId) {
throw new CRPCError({
code: 'FORBIDDEN',
message: 'Personal organizations can only be deleted by deleting your account.',
});
}
// Switch to personal org first
await ctx.auth.api.setActiveOrganization({
body: { organizationId: ctx.user.personalOrganizationId! },
headers: ctx.auth.headers,
});
await ctx.auth.api.deleteOrganization({
body: { organizationId },
headers: ctx.auth.headers,
});
return null;
});Set Active Organization
export const setActive = authMutation
.input(z.object({ organizationId: zid('organization') }))
.output(z.null())
.mutation(async ({ ctx, input }) => {
await ctx.auth.api.setActiveOrganization({
body: { organizationId: input.organizationId },
headers: ctx.auth.headers,
});
return null;
});Invitation Functions
Send Invitation
export const inviteMember = authMutation
.input(
z.object({
email: z.string().email(),
role: z.enum(['owner', 'member']),
})
)
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { invitation: ['create'] } });
await ctx.auth.api.createInvitation({
body: {
email: input.email,
role: input.role,
organizationId: ctx.user.activeOrganization!.id,
},
headers: ctx.auth.headers,
});
return null;
});Accept Invitation
export const acceptInvitation = authMutation
.input(z.object({ invitationId: zid('invitation') }))
.output(z.null())
.mutation(async ({ ctx, input }) => {
const invitation = await ctx.table('invitation').get(input.invitationId);
if (!invitation || invitation.email !== ctx.user.email) {
throw new CRPCError({
code: 'FORBIDDEN',
message: 'Invitation not found',
});
}
if (invitation.status !== 'pending') {
throw new CRPCError({
code: 'BAD_REQUEST',
message: 'Invitation already processed',
});
}
await ctx.auth.api.acceptInvitation({
body: { invitationId: input.invitationId },
headers: ctx.auth.headers,
});
return null;
});Reject Invitation
export const rejectInvitation = authMutation
.input(z.object({ invitationId: zid('invitation') }))
.output(z.null())
.mutation(async ({ ctx, input }) => {
const invitation = await ctx.table('invitation').get(input.invitationId);
if (!invitation || invitation.email !== ctx.user.email) {
throw new CRPCError({
code: 'FORBIDDEN',
message: 'Invitation not found',
});
}
await ctx.auth.api.rejectInvitation({
body: { invitationId: input.invitationId },
headers: ctx.auth.headers,
});
return null;
});Cancel Invitation
export const cancelInvitation = authMutation
.input(z.object({ invitationId: zid('invitation') }))
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { invitation: ['cancel'] } });
await ctx.auth.api.cancelInvitation({
body: { invitationId: input.invitationId },
headers: ctx.auth.headers,
});
return null;
});List User Invitations
Get invitations sent to the current user:
export const listUserInvitations = authQuery
.output(
z.array(
z.object({
id: zid('invitation'),
organizationName: z.string(),
organizationSlug: z.string(),
role: z.string(),
inviterName: z.string().nullable(),
expiresAt: z.number(),
})
)
)
.query(async ({ ctx }) => {
const invitations = await ctx
.table('invitation', 'email', (q) => q.eq('email', ctx.user.email))
.filter((q) => q.eq(q.field('status'), 'pending'));
return asyncMap(invitations, async (inv) => {
const org = await inv.edgeX('organization');
const inviter = await inv.edgeX('inviter');
return {
id: inv._id,
organizationName: org.name,
organizationSlug: org.slug,
role: inv.role || 'member',
inviterName: inviter.name,
expiresAt: inv.expiresAt,
};
});
});List Pending Invitations
export const listPendingInvitations = authQuery
.input(z.object({ slug: z.string() }))
.output(
z.array(
z.object({
id: zid('invitation'),
email: z.string(),
role: z.string(),
expiresAt: z.number(),
})
)
)
.query(async ({ ctx, input }) => {
const org = await ctx.table('organization').get('slug', input.slug);
if (!org || ctx.user.activeOrganization?.id !== org._id) return [];
const invitations = await ctx
.table('invitation', 'organizationId_status', (q) =>
q.eq('organizationId', org._id).eq('status', 'pending')
)
.take(100);
return invitations.map((inv) => ({
id: inv._id,
email: inv.email,
role: inv.role || 'member',
expiresAt: inv.expiresAt,
}));
});Member Functions
Get Active Member
Get the current user's membership in the active organization:
export const getActiveMember = authQuery
.output(
z.object({
id: zid('member'),
role: z.string(),
createdAt: z.number(),
}).nullable()
)
.query(async ({ ctx }) => {
if (!ctx.user.activeOrganization) return null;
const member = await ctx
.table('member', 'organizationId_userId', (q) =>
q
.eq('organizationId', ctx.user.activeOrganization!.id)
.eq('userId', ctx.userId)
)
.first();
if (!member) return null;
return {
id: member._id,
role: member.role,
createdAt: member.createdAt,
};
});Add Member Directly
Add a member without invitation (admin use):
export const addMember = authMutation
.input(
z.object({
userId: zid('user'),
role: z.enum(['owner', 'member']),
})
)
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { member: ['create'] } });
await ctx.auth.api.addMember({
body: {
userId: input.userId,
organizationId: ctx.user.activeOrganization!.id,
role: input.role,
},
headers: ctx.auth.headers,
});
return null;
});List Members
export const listMembers = authQuery
.input(z.object({ slug: z.string() }))
.output(
z.object({
members: z.array(
z.object({
id: zid('member'),
role: z.string(),
user: z.object({
id: zid('user'),
name: z.string().nullable(),
email: z.string(),
image: z.string().nullish(),
}),
})
),
})
)
.query(async ({ ctx, input }) => {
const org = await ctx.table('organization').get('slug', input.slug);
if (!org) return { members: [] };
const members = await org.edge('members').take(100);
const enrichedMembers = await Promise.all(
members.map(async (member) => {
const user = await member.edgeX('user');
return {
id: member._id,
role: member.role,
user: {
id: user._id,
name: user.name,
email: user.email,
image: user.image,
},
};
})
);
return { members: enrichedMembers };
});Update Member Role
export const updateMemberRole = authMutation
.input(
z.object({
memberId: zid('member'),
role: z.enum(['owner', 'member']),
})
)
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { member: ['update'] } });
await ctx.table('member').getX(input.memberId).patch({ role: input.role });
return null;
});Remove Member
export const removeMember = authMutation
.input(z.object({ memberId: zid('member') }))
.output(z.null())
.mutation(async ({ ctx, input }) => {
await hasPermission(ctx, { permissions: { member: ['delete'] } });
await ctx.auth.api.removeMember({
body: {
memberIdOrEmail: input.memberId,
organizationId: ctx.user.activeOrganization?.id,
},
headers: ctx.auth.headers,
});
return null;
});Leave Organization
export const leave = authMutation
.output(z.null())
.mutation(async ({ ctx }) => {
// Prevent last owner from leaving
if (ctx.user.activeOrganization?.role === 'owner') {
const owners = await ctx
.table('member', 'organizationId_role', (q) =>
q
.eq('organizationId', ctx.user.activeOrganization!.id)
.eq('role', 'owner')
)
.take(2);
if (owners.length <= 1) {
throw new CRPCError({
code: 'FORBIDDEN',
message: 'Cannot leave as the only owner',
});
}
}
await ctx.auth.api.leaveOrganization({
body: { organizationId: ctx.user.activeOrganization!.id },
headers: ctx.auth.headers,
});
return null;
});Teams
Teams allow grouping members within an organization.
Create Team
export const createTeam = authMutation
.input(z.object({ name: z.string().min(1).max(100) }))
.output(zid('team'))
.mutation(async ({ ctx, input }) => {
const team = await ctx.auth.api.createTeam({
body: {
name: input.name,
organizationId: ctx.user.activeOrganization!.id,
},
headers: ctx.auth.headers,
});
return team.id;
});Team Operations
// List teams
const teams = await ctx.auth.api.listTeams({
query: { organizationId: ctx.user.activeOrganization!.id },
headers: ctx.auth.headers,
});
// Add member to team
await ctx.auth.api.addTeamMember({
body: { teamId, userId },
headers: ctx.auth.headers,
});
// Remove member from team
await ctx.auth.api.removeTeamMember({
body: { teamId, userId },
headers: ctx.auth.headers,
});
// List team members
const members = await ctx.auth.api.listTeamMembers({
body: { teamId },
headers: ctx.auth.headers,
});Hooks
Hooks allow custom logic during organization lifecycle events.
Organization Hooks
organization({
// Organization creation
organizationCreation: {
beforeCreate: async ({ organization, user }) => {
// Validate or modify before creation
return { data: organization };
},
afterCreate: async ({ organization, member, user }) => {
// Setup default resources, create initial data
},
},
// Organization deletion
organizationDeletion: {
beforeDelete: async (data) => {
// Cleanup related resources before deletion
},
afterDelete: async (data) => {
// Post-deletion cleanup
},
},
}),Member Hooks
organization({
membershipManagement: {
beforeAddMember: async ({ organization, member, user }) => {
// Validate member addition
return { data: member };
},
afterAddMember: async ({ organization, member, user }) => {
// Post-addition logic (notifications, etc.)
},
beforeRemoveMember: async ({ organization, member, user }) => {
// Cleanup before removal
},
afterRemoveMember: async ({ organization, member, user }) => {
// Post-removal logic
},
beforeUpdateRole: async ({ organization, member, role }) => {
return { data: { role } };
},
afterUpdateRole: async ({ organization, member, role }) => {
// Role change notifications
},
},
}),Invitation Hooks
organization({
invitationManagement: {
beforeCreateInvitation: async ({ invitation, organization, inviter }) => {
return { data: invitation };
},
afterCreateInvitation: async ({ invitation, organization, inviter }) => {
// Send custom notification
},
beforeAcceptInvitation: async ({ invitation, user }) => {
return { data: invitation };
},
afterAcceptInvitation: async ({ invitation, member, user }) => {
// Welcome logic
},
},
}),Team Hooks
organization({
teamManagement: {
beforeCreateTeam: async ({ team, organization }) => {
return { data: team };
},
afterCreateTeam: async ({ team, organization }) => {},
beforeAddTeamMember: async ({ team, user }) => {
return { data: { team, user } };
},
afterAddTeamMember: async ({ team, user }) => {},
},
}),Client Usage
React Hooks
import { authClient } from '@/lib/convex/auth-client';
function OrgSwitcher() {
// Active organization (reactive)
const { data: activeOrg } = authClient.useActiveOrganization();
// List organizations (reactive)
const { data: orgs } = authClient.useListOrganizations();
// Set active organization
const handleSwitch = (orgId: string) => {
authClient.organization.setActive({ organizationId: orgId });
};
// Create organization
const handleCreate = () => {
authClient.organization.create({ name: 'New Org', slug: 'new-org' });
};
return (
<select onChange={(e) => handleSwitch(e.target.value)}>
{orgs?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
);
}Get Full Organization
// Get full organization details (non-reactive)
const { data: fullOrg } = await authClient.organization.getFullOrganization({
query: { organizationId: orgId },
});
// Returns: organization + members + invitationsInvitation Operations
// Get user's pending invitations
const { data: invitations } = await authClient.organization.listInvitations();
// Accept invitation
await authClient.organization.acceptInvitation({
invitationId: 'inv_123',
});
// Reject invitation
await authClient.organization.rejectInvitation({
invitationId: 'inv_123',
});
// Cancel invitation (as org admin)
await authClient.organization.cancelInvitation({
invitationId: 'inv_123',
});Member Operations
// Get active member info
const { data: member } = await authClient.organization.getActiveMember();
// Leave organization
await authClient.organization.leave();
// Remove member (as admin)
await authClient.organization.removeMember({
memberIdOrEmail: 'member_123',
});
// Update member role
await authClient.organization.updateMemberRole({
memberId: 'member_123',
role: 'admin',
});Permission Check
// Check permission before showing UI
const { data } = await authClient.organization.hasPermission({
permissions: { organization: ['delete'] },
});
if (data?.success) {
// Show delete button
}Team Operations (Client)
// List teams
const { data: teams } = await authClient.organization.listTeams();
// Create team
await authClient.organization.createTeam({
name: 'Engineering',
});
// Set active team (stored in session)
await authClient.organization.setActiveTeam({
teamId: 'team_123',
});API Reference
| Operation | Method | Multi-table |
|---|---|---|
| Create org | Better Auth API | Yes |
| Update org | Direct table | No |
| Delete org | Better Auth API | Yes |
| List orgs | Direct table | No |
| Check slug | Direct table | No |
| Invite member | Better Auth API | Yes |
| Accept invite | Better Auth API | Yes |
| Reject invite | Better Auth API | Yes |
| Cancel invite | Better Auth API | Yes |
| List user invites | Direct table | No |
| Add member | Better Auth API | Yes |
| Update role | Direct table | No |
| Remove member | Better Auth API | Yes |
| Leave org | Better Auth API | Yes |
| Create team | Better Auth API | Yes |
| Add team member | Better Auth API | Yes |
| Remove team member | Better Auth API | Yes |
Use Better Auth API for operations that modify multiple tables. Use direct table operations for simple single-table reads/updates.