Multi Tenant Kit

Launch B2B SaaS with support for multiple organizations and plans, comes with a super admin dashboard to manage organizations and plans.

Multi Tenant Kit 🏒

Build powerful multi-tenant B2B SaaS applications with organization management, role-based access control, and subscription billing! ✨

What is Multi Tenant Kit? πŸ€”

The Multi Tenant Kit transforms Indie Kit into a complete B2B SaaS platform with:

  • 🏒 Multi-Organization Support: Users can belong to multiple organizations
  • πŸ‘₯ Role-Based Access Control: Owner, Admin, and User roles with permissions
  • πŸ’³ Subscription Management: Integrate with Stripe or LemonSqueezy
  • πŸ‘‘ Super Admin Dashboard: Manage all organizations and plans
  • πŸ“Š Plan Quotas: Limit features based on subscription tiers
  • βœ‰οΈ Team Invitations: Invite members to your organization
  • πŸ” Secure API Routes: Built-in authentication wrappers

Getting Started

Multi Tenant Kit is a separate boilerplate that extends Indie Kit with multi-tenancy features. Perfect for building tools like Slack, Notion, or Figma!

Installation πŸš€

If you don't have the Multi Tenant Kit yet:

git clone https://github.com/Indie-Kit/b2b-boilerplate
cd b2b-boilerplate
pnpm install

License Required

You need a valid Indie Kit B2B license to access this repository. Get your license here!

Quick Navigation πŸ—ΊοΈ

Jump to what you need:

Database Structure πŸ—„οΈ

The Multi Tenant Kit uses a relational database structure designed for multi-tenancy:

Key Relationships πŸ”—

  • Users ↔ Organizations: Many-to-many through OrganizationMembership
  • Organizations β†’ Plan: Each organization has one plan (or null for free tier)
  • Organizations β†’ Invites: Organizations can send multiple invitations
  • Organizations β†’ Billing: Connected to Stripe/LemonSqueezy for payments

Database Schema

All tables use Drizzle ORM. Check src/db/schema/ for the complete type-safe schema definitions!

Organizations 🏒

Organizations are the core of the Multi Tenant Kit! Users can create and join multiple organizations, each with its own team, subscription, and settings.

How Organizations Work πŸ”„

Think of organizations like workspaces in Slack or teams in Figma:

  • πŸ‘€ Multiple Users: An organization can have many team members
  • πŸ‘₯ Multiple Organizations Per User: Users can belong to many organizations
  • 🎭 Different Roles: Each user has a specific role in each organization
  • πŸ’³ Independent Billing: Each organization has its own subscription
  • βš™οΈ Isolated Data: Organization data is completely separated

Organization Features ✨

1. Unique Identity

  • ID, name, and slug for URL routing
  • Optional organization image/logo
  • Timestamps for tracking

2. Role-Based Access Control

  • Owner: Full control (billing, deletion, everything)
  • Admin: Manage members and settings
  • User: Basic access to features

3. Onboarding System

  • Track if onboarding is complete
  • Store onboarding data (JSON)
  • Custom onboarding flows

4. Billing Integration

  • Stripe or LemonSqueezy support
  • Customer and subscription IDs stored
  • Automatic plan assignment

5. Plan Management

  • Each organization has a plan (or null for free)
  • Plan quotas control feature access
  • Easy plan upgrades/downgrades

Organization Context

The current organization is stored in a cookie session! When you log in, your first organization is automatically selected. Use switchOrganization() to change the active organization without navigating away.

Authentication & Security πŸ”

The Multi Tenant Kit includes powerful authentication wrappers to protect your API routes. Choose the right wrapper for your security needs!

Three Levels of Protection πŸ›‘οΈ

WrapperProtection LevelUse Case
withAuthRequiredUser must be logged inPersonal data, user settings
withOrganizationAuthRequiredUser + Organization + RoleOrganization resources, team features
withSuperAdminAuthRequiredSuper admin onlyPlatform management, all organizations

1. Basic Authentication: withAuthRequired πŸ‘€

Requires user to be logged in. Perfect for personal features:

// src/app/api/app/profile/route.ts
import withAuthRequired from "@/lib/auth/withAuthRequired";

export const GET = withAuthRequired(async (req, context) => {
  const user = await context.session.user;
  
  // Fetch user's personal data
  const profile = await getUserProfile(user.id);
  
  return NextResponse.json(profile);
});

Use this for:

  • User profile endpoints
  • Personal settings
  • User-specific data (not org-specific)

2. Organization Authentication: withOrganizationAuthRequired 🏒

Requires user to belong to the organization + optional role check:

// src/app/api/app/org/[slug]/settings/route.ts
import withOrganizationAuthRequired from "@/lib/auth/withOrganizationAuthRequired";
import { OrganizationRole } from "@/db/schema/organization";

export const PATCH = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization;
    const body = await req.json();
    
    // Update organization settings
    await updateSettings(organization.id, body);
  
  return NextResponse.json({ success: true });
  },
  OrganizationRole.enum.admin // Only admin or owner
);

Use this for:

  • Organization resources (projects, documents, etc.)
  • Team settings
  • Member management
  • Anything scoped to an organization

Learn more about Organization Auth β†’

3. Super Admin Authentication: withSuperAdminAuthRequired πŸ‘‘

Requires super admin privileges. For platform management only:

// src/app/api/super-admin/organizations/route.ts
import withSuperAdminAuthRequired from "@/lib/auth/withSuperAdminAuthRequired";

export const GET = withSuperAdminAuthRequired(async (req, context) => {
  // Fetch all organizations (admin view)
  const allOrgs = await getAllOrganizations();
  
  return NextResponse.json(allOrgs);
});

Use this for:

  • Managing all organizations
  • Creating/editing plans
  • Platform analytics
  • User impersonation

Security Best Practice

Always use the most restrictive wrapper for your use case! If an endpoint needs organization context, use withOrganizationAuthRequired, not just withAuthRequired.

Roles & Permissions πŸ‘₯

The Multi Tenant Kit uses a simple but powerful three-tier role system:

export const roleEnum = pgEnum("role", ["owner", "admin", "user"]);

Role Hierarchy πŸ“Š

πŸ‘‘ Owner (Level 2)
  ↓ Can do everything
πŸ‘€ Admin (Level 1)
  ↓ Can do most things
πŸ™‚ User (Level 0)
  ↓ Can use features

Role Permissions πŸ”‘

PermissionOwnerAdminUser
Delete organizationβœ…βŒβŒ
Manage billingβœ…βŒβŒ
Invite/remove membersβœ…βœ…βŒ
Change member rolesβœ…βœ…βŒ
Update org settingsβœ…βœ…βŒ
Use featuresβœ…βœ…βœ…

Role Checking in Code πŸ’»

The hasHigherOrEqualRole function compares roles:

const roleHierarchy = {
  user: 0,
  admin: 1,
  owner: 2,
};

// Check if user can perform admin actions
if (hasHigherOrEqualRole({ 
  currentRole: userRole, 
  requiredRole: 'admin' 
})) {
  // Allow action
}

This means:

  • Owners can do everything Admins and Users can do
  • Admins can do everything Users can do
  • Users have only their own permissions

Learn more about managing roles β†’

Invitations & Memberships βœ‰οΈ

Build your team by inviting members to your organization!

How Memberships Work πŸ‘₯

Organization memberships connect users to organizations:

OrganizationMembership {
  organizationId: string  // Which organization
  userId: string          // Which user
  role: 'owner' | 'admin' | 'user'  // Their role
  createdAt: timestamp
  updatedAt: timestamp
}

Key Features:

  • One user can belong to multiple organizations
  • Each membership has a specific role
  • Composite primary key prevents duplicates
  • Timestamps track when they joined

The Invitation System πŸ“¨

Admins and Owners can invite new members:

OrganizationInvite {
  id: string              // Unique invite ID
  email: string           // Invitee's email
  organizationId: string  // Target organization
  role: 'owner' | 'admin' | 'user'  // Assigned role
  token: string           // Verification token
  expiresAt: timestamp    // When invite expires
  createdAt: timestamp
}

How it Works:

  1. Admin/Owner sends invite with email and role
  2. System creates invite with unique token
  3. Email sent to invitee with link
  4. Invitee clicks link, creates account (if needed)
  5. Membership created automatically
  6. Invite marked as used or deleted

Invitation Expiry

Invitations expire after a set period (default: 7 days). This prevents unused invites from piling up and improves security!

Learn more about inviting members β†’

Plans & Subscriptions πŸ’³

Monetize your B2B SaaS with flexible subscription plans!

Understanding Plans πŸ“Š

Plans define what organizations can do and how much they pay:

Plan Structure:

  • Name and codename (for URLs)
  • Default flag (free tier)
  • Pricing tiers (monthly, yearly, one-time)
  • Payment provider integration
  • Feature quotas

Supported Pricing Models:

  • πŸ’° Monthly: Recurring monthly charge
  • πŸ“… Yearly: Annual subscription (usually discounted)
  • 🎯 One-time: Lifetime access or credits

Payment Providers:

  • Stripe (recommended)
  • LemonSqueezy

Creating Plans (Super Admin) πŸ‘‘

Plans are managed through the super admin dashboard:

  1. Navigate to /super-admin/plans
  2. Click "Create Plan"
  3. Fill in the details:
Plan Details:
β”œβ”€ Name: "Professional"
β”œβ”€ Codename: "pro"
β”œβ”€ Default: false
β”œβ”€ Monthly Price: $49
β”œβ”€ Yearly Price: $490 (save $98!)
β”œβ”€ Stripe Price IDs
└─ Quotas: { projects: 100, users: 50 }
  1. Save and it's live!

Plan Quotas

Quotas are stored as JSON and can be anything: { projects: 100, storage: "50GB", apiCalls: 10000 }. Check quotas in your code to limit features!

Use getSubscribeUrl to create subscription links:

import Link from "next/link";
import getSubscribeUrl from "@/lib/plans/getSubscribeUrl";
import { PlanType, PlanProvider } from "@/lib/plans/getSubscribeUrl";

export function PricingCard() {
  return (
    <div className="pricing-card">
      <h3>Pro Plan</h3>
      <p>$49/month</p>
      
      <Link
        href={getSubscribeUrl({
  codename: "pro",
  type: PlanType.MONTHLY,
  provider: PlanProvider.STRIPE,
          trialPeriodDays: 7  // Optional 7-day trial
        })}
        className="btn-primary"
      >
        Start Free Trial
</Link>
    </div>
  );
}

getSubscribeUrl Parameters:

  • codename: Plan identifier (e.g., "pro", "enterprise")
  • type: MONTHLY, YEARLY, or ONETIME
  • provider: STRIPE or LEMON_SQUEEZY
  • trialPeriodDays: Optional trial period

Checking Plan Quotas in Code πŸ“

Enforce plan limits in your application:

'use client'

import { useOrganization } from '@/lib/organizations/useOrganization'

export function CreateProjectButton() {
  const { organization } = useOrganization()
  
  // Check if they can create more projects
  const currentProjects = 45  // From your database
  const maxProjects = organization.plan?.quotas.projects || 10
  const canCreate = currentProjects < maxProjects
  
  return (
    <button disabled={!canCreate}>
      {canCreate ? 'Create Project' : `Upgrade to create more (${currentProjects}/${maxProjects})`}
    </button>
  )
}

Hooks & Utilities πŸͺ

The Multi Tenant Kit provides powerful React hooks and utility functions for your application!

useOrganization Hook 🏒

Your go-to hook for accessing organization data in client components:

'use client'

import { useOrganization } from '@/lib/organizations/useOrganization'

export default function Dashboard() {
  const {
    organization,      // Current org data with plan and role
    isLoading,         // Loading state
    error,             // Error state  
    mutate,            // Refresh data
    switchOrganization // Switch to another org
  } = useOrganization()
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error loading organization</div>
  
  return (
    <div>
      <h1>{organization.name}</h1>
      <p>Plan: {organization.plan?.name}</p>
      <p>Your Role: {organization.role}</p>
    </div>
  )
}

What you get:

  • Organization name, slug, image
  • Current user's role in the organization
  • Plan details and quotas
  • Loading and error states
  • Functions to refresh or switch organizations

Full useOrganization documentation β†’

Organization Utility Functions πŸ› οΈ

Server-side utilities for working with organizations:

getUserOrganizations(userId)

Fetches all organizations a user belongs to:

const userOrgs = await getUserOrganizations(userId);
// Returns: Array<{id, name, slug, image, role, onboardingDone}>

getUserOrganizationById(userId, organizationId)

Fetches a specific organization with plan:

const org = await getUserOrganizationById(userId, organizationId);
// Returns: {id, name, slug, role, plan: {id, name, quotas, ...}}

createOrganization(name, creatorId)

Creates a new organization:

const newOrg = await createOrganization("Acme Corp", userId);
// Creator automatically becomes owner

hasHigherOrEqualRole({currentRole, requiredRole})

Checks if a role meets requirements:

if (hasHigherOrEqualRole({
  currentRole: 'admin',
  requiredRole: 'admin'
})) {
  // Allow action
}

userBelongsToOrganization(userId, organizationId)

Verifies membership:

const isMember = await userBelongsToOrganization(userId, orgId);
// Returns: boolean

Type Safety

All utilities are fully typed with TypeScript! Your IDE will autocomplete everything.

Quick Start Guide πŸš€

Ready to build? Here's how to get started:

1. Install Multi Tenant Kit βœ…

git clone https://github.com/Indie-Kit/b2b-boilerplate
cd b2b-boilerplate
pnpm install

2. Set Up Database πŸ—„οΈ

pnpm db:push  # Push schema to database

3. Create Your First Plan πŸ’³

  1. Run the app: pnpm dev
  2. Make yourself super admin (check docs)
  3. Go to /super-admin/plans
  4. Create a "Free" plan with default: true
  5. Create paid plans (Pro, Enterprise, etc.)

4. Build Organization Features 🏒

Use the provided hooks and wrappers:

// Client component with organization data
'use client'
import { useOrganization } from '@/lib/organizations/useOrganization'

// API route with organization auth
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'

5. Test Everything ✨

  • Create an organization
  • Invite team members
  • Subscribe to a plan
  • Test role permissions
  • Verify quota enforcement

Deep dive into specific topics:

Session-Based Context

Remember: The current organization is managed via cookie session, not URL routing! This means users stay on the same URL when switching organizations - the context changes seamlessly in the background.

Need Help? πŸ’¬

Start Building!

You now have everything you need to build a powerful B2B SaaS application! Start with the Quick Start Guide above and refer back to specific sections as needed. πŸš€