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 - Understanding the data models
- Organizations - How organizations work
- Authentication - Securing your routes
- Roles & Permissions - Managing access control
- Invitations - Team management
- Plans & Billing - Subscription management
- Hooks & Utilities - Developer tools
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 π‘οΈ
Wrapper | Protection Level | Use Case |
---|---|---|
withAuthRequired | User must be logged in | Personal data, user settings |
withOrganizationAuthRequired | User + Organization + Role | Organization resources, team features |
withSuperAdminAuthRequired | Super admin only | Platform 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 π
Permission | Owner | Admin | User |
---|---|---|---|
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:
- Admin/Owner sends invite with email and role
- System creates invite with unique token
- Email sent to invitee with link
- Invitee clicks link, creates account (if needed)
- Membership created automatically
- 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:
- Navigate to
/super-admin/plans
- Click "Create Plan"
- 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 }
- 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!
Generating Subscription Links π
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 ONETIMEprovider
: STRIPE or LEMON_SQUEEZYtrialPeriodDays
: 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 π³
- Run the app:
pnpm dev
- Make yourself super admin (check docs)
- Go to
/super-admin/plans
- Create a "Free" plan with default: true
- 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
Related Documentation π
Deep dive into specific topics:
- Organization Roles - Managing permissions
- useOrganization Hook - Client-side hook
- withOrganizationAuthRequired - API security
- Inviting Team Members - Team management
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? π¬
- Check the Indie Kit Docs
- Join the Discord Community
- Contact support for license holders
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. π