Skip to content

Organizations, Teams & Billing

Multi-tenant structure with Stripe integration for billing.

Multi-Tenancy Model

Organization
├── Members (owner, admin, member)
├── Teams
│   └── Team Members
├── Settings
├── Billing/Subscription
└── Metadata (serverHostURL, etc.)

Organizations

Organizations are the top-level tenant boundary.

Creating Organizations

/profile/organizations/create

// Using BetterAuth
const { data: org } = await authClient.organization.create({
  name: "My Organization",
  slug: "my-org"
});

Listing Organizations

// User's organizations
const orgs = authClient.useListOrganizations();

// Displayed in profile
<UserOrganizationsTable organizations={orgs} />

Active Organization

The active organization is synced with the URL:

// In useAuth hook
useEffect(() => {
  if (organizationSlug) {
    authClient.organization.setActive({ organizationSlug });
  }
}, [organizationSlug]);

Organization Settings

/portal/[slug]/settings

Settings pages for: - Members management - Teams management - Billing configuration

Organization Members

/portal/[slug]/settings/members

Member Roles

Role Capabilities
owner Full access, billing, delete org
admin Member/team management, training
member Basic read/write access

Listing Members

const { data: members } = useQuery({
  queryKey: ["org-members", orgId],
  queryFn: () => authClient.organization.listMembers({
    organizationId: orgId,
    limit: 100,
    sortBy: "createdAt",
    sortDirection: "desc"
  })
});

<OrgMembersTable members={members} />

Inviting Members

await authClient.organization.inviteMember({
  organizationId,
  email: "[email protected]",
  role: "member",
  teamIds: ["team-123"] // Optional team assignment
});

Components: - InviteOrgMemberModal - Invitation form - OrgInvitesTable - Pending invitations

Managing Invitations

// Cancel invitation
await authClient.organization.cancelInvitation({ invitationId });

// Accept invitation (from invitee's perspective)
await authClient.organization.acceptInvitation({ invitationId });

Teams

Teams are sub-groups within organizations.

Creating Teams

/portal/[slug]/settings/teams/new

await authClient.organization.createTeam({
  organizationId,
  name: "Varsity Football",
  // Additional metadata
});

Listing Teams

const { data: teams } = useQuery({
  queryKey: ["org-teams", orgId],
  queryFn: () => authClient.organization.listTeams({ organizationId: orgId })
});

// Filter out the default org-name team
const displayTeams = teams.filter(t => t.name !== org.name);

<OrgTeamsTable teams={displayTeams} />

Active Team

Teams are activated from the URL:

// In useTeam hook
useEffect(() => {
  if (teamId) {
    authClient.organization.setActiveTeam({ teamId });
  }
}, [teamId]);

Team Members

/portal/[slug]/[teamID]/settings/team

// List team members
const { data: teamMembers } = useQuery({
  queryKey: ["team-members", teamId],
  queryFn: () => authClient.organization.listTeamMembers({ teamId })
});

// Get user details for each member
const memberDetails = await Promise.all(
  teamMembers.map(m => authClient.admin.getUser({ userId: m.userId }))
);

Components: - InviteToTeamForm - Add org member to team - TeamMembersTable - Team roster

Billing (Stripe)

Configuration

// lib/stripe.ts
import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

BetterAuth Stripe Plugin

The auth client includes Stripe integration:

// In lib/auth-client.ts
stripeClient({ subscription: true })

This enables: - Subscription management - Customer portal access - Webhook handling (backend)

Payment Flow

/portal/[slug]/pay

import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe";

<Elements stripe={stripePromise} options={elementOptions}>
  <StripePaymentForm organizationId={orgId} />
</Elements>

Payment Form

components/forms/stripe-payment-form.tsx

Handles: - Card input via Stripe Elements - Payment intent creation - Confirmation flow - Error handling

Success Page

/portal/[slug]/pay/success

Post-checkout confirmation with: - Success message - Subscription details - Continue to portal button

Billing Settings

/portal/[slug]/settings/billing

Management interface for: - Current subscription status - Payment method - Invoice history - Cancel/upgrade subscription

Components

Organization Tables

Component Purpose
OrgMembersTable Organization member list
OrgInvitesTable Pending invitations
OrgTeamsTable Teams list
UserOrganizationsTable User's organizations (profile)
UserOrganizationsListTable Alternative org list view

Team Tables

Component Purpose
TeamMembersTable Team member roster
TeamDevicesTable Team devices

Forms

Component Purpose
NewOrganizationForm Create organization
UpdateOrganizationForm Edit organization
NewTeamForm Create team
UpdateTeamForm Edit team
InviteOrgMemberModal Invite to org
InviteToTeamForm Add to team

Modals

Component Purpose
DeleteOrgModal Confirm org deletion
LeaveOrgModal Confirm leaving org
DeleteTeamModal Confirm team deletion

Access Patterns

Organization-Scoped Data

Most data is scoped to the organization:

const { data } = useQuery({
  queryKey: ["sessions", organizationSlug],
  queryFn: () => fetchJSON(`/sessions?org=${organizationSlug}`, token)
});

Team-Scoped Data

Some features filter by team:

const { data } = useQuery({
  queryKey: ["chat", organizationSlug, teamId],
  queryFn: () => fetchJSON(`/chats?team=${teamId}`, token)
});

Invitation Flow

  1. Org Owner/Admin sends invitation
  2. Invitee receives email with link
  3. Invitee visits /portal/[slug]/accept-invite/[inviteID]
  4. Invitee accepts (must be logged in)
  5. Invitee added to org with specified role
// Accept invite page
const acceptInvite = async () => {
  await authClient.organization.acceptInvitation({ invitationId });
  router.push(`/portal/${organizationSlug}`);
};