@bettercone/ui
Guides

Organizations

Multi-tenant organizations in BetterCone with role-based access control

BetterCone includes multi-tenant organization support with Better Auth's organization plugin, enabling team-based B2B SaaS features.

What's Included

  • Organizations - Create and manage teams
  • Members - Invite users with role-based access
  • Invitations - Email-based team invitations
  • Roles - owner, admin, member permissions
  • Subscriptions - Organization-level Stripe billing

Quick Start

Client Setup

// apps/web/src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { organizationClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    organizationClient(),
    // ... other plugins
  ],
});

Server Setup

// packages/convex/convex/auth.ts
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";

export const createAuth = (ctx) => {
  return betterAuth({
    plugins: [
      organization({
        sendInvitationEmail: async (data) => {
          // Send email with invitation link
          console.log(`Invite ${data.email} to ${data.organization.name}`);
          console.log(`Link: /auth/accept-invitation?invitationId=${data.invitation.id}`);
        },
      }),
    ],
  });
};

Creating Organizations

import { authClient } from "@/lib/auth-client";

// Create organization
const { data: org } = await authClient.organization.create({
  name: "Acme Corp",
  slug: "acme-corp",
});

// Returns:
// {
//   id: "org_123",
//   name: "Acme Corp",
//   slug: "acme-corp",
//   createdAt: Date,
//   metadata: {}
// }

Managing Members

// Invite member
await authClient.organization.inviteMember({
  organizationId: org.id,
  email: "member@example.com",
  role: "member", // "owner" | "admin" | "member"
});

// List members
const { data: members } = await authClient.organization.listMembers({
  organizationId: org.id,
});

// Update member role
await authClient.organization.updateMemberRole({
  organizationId: org.id,
  userId: member.id,
  role: "admin",
});

// Remove member
await authClient.organization.removeMember({
  organizationId: org.id,
  userId: member.id,
});

Roles & Permissions

BetterCone uses three roles:

RolePermissions
ownerFull access, delete org, manage billing
adminManage members, invite users, settings
memberRead-only, basic features

Check permissions in components:

import { useActiveOrganization } from "@/lib/auth-client";

const { data: org } = useActiveOrganization();

if (org?.role === "owner" || org?.role === "admin") {
  // Show admin features
}

Server-Side Usage

// In API routes or server components
import { auth } from "@repo/convex/convex/auth";

export async function GET(req: Request) {
  const session = await auth.api.getSession({ headers: req.headers });
  
  if (!session?.session.activeOrganizationId) {
    return Response.json({ error: "No active organization" }, { status: 401 });
  }
  
  const orgId = session.session.activeOrganizationId;
  // Use orgId for queries
}

Switching Organizations

// Set active organization
await authClient.organization.setActive({
  organizationId: org.id,
});

// Get active organization
const { data: activeOrg } = useActiveOrganization();

Organization Subscriptions

Organizations have Stripe subscriptions:

import { useQuery } from "convex/react";
import { api } from "@repo/convex";

const features = useQuery(api.usage.getFeatureAccess, {
  organizationId: org.id,
});

// Returns:
// {
//   advancedAnalytics: false,
//   apiAccess: true,
//   customIntegrations: false,
//   planId: "free"
// }

See Stripe Guide for billing setup.

Usage Tracking

Track usage per organization:

const usage = useQuery(api.usage.getCurrentUsage, {
  userId: user.id,
  organizationId: org.id,
});

// Returns:
// {
//   apiCalls: 1543,
//   apiLimit: 10000,  // Based on subscription plan
//   period: "2024-10"
// }

Invitation Flow

  1. Admin invites member:
await authClient.organization.inviteMember({
  organizationId: org.id,
  email: "newuser@example.com",
  role: "member",
});
  1. BetterCone sends invitation email (configure in auth.ts):
sendInvitationEmail: async (data) => {
  await sendEmail({
    to: data.email,
    subject: `Join ${data.organization.name}`,
    html: `Click to accept: ${siteUrl}/auth/accept-invitation?invitationId=${data.invitation.id}`,
  });
}
  1. User accepts invitation:
// Navigate to /auth/accept-invitation?invitationId=inv_123
await authClient.organization.acceptInvitation({
  invitationId: "inv_123",
});

Best Practices

  1. Always check organization context - Most queries should include organizationId
  2. Enforce permissions - Check roles before sensitive operations
  3. Use organization-scoped data - Index tables by organizationId
  4. Set active organization - Users with multiple orgs need active selection
  5. Handle invitation emails - Configure email service in production

Environment Variables

# In production, configure email service
RESEND_API_KEY=re_...
SITE_URL=https://yourdomain.com

Learn More