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:
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¶
- Org Owner/Admin sends invitation
- Invitee receives email with link
- Invitee visits
/portal/[slug]/accept-invite/[inviteID] - Invitee accepts (must be logged in)
- Invitee added to org with specified role