Organizations & Tenancy
Understand multi-tenant isolation, membership roles, and how the active org shapes access control.
Welcome to the Organizations & Tenancy guide! If you're building a SaaS application, multi-tenancy is crucial for scaling and isolating customer data. This guide will walk you through how the SaaS Boilerplate implements organizations as the core tenant model, ensuring secure data isolation while allowing users to belong to multiple organizations. We'll cover the data model, roles, API scoping, and practical examples to help you understand and extend this system.
Why Multi-Tenancy Matters
In a SaaS application, multi-tenancy means serving multiple customers (tenants) from a single codebase and database, while keeping their data completely separate. Organizations in the SaaS Boilerplate represent these tenants—each company or team gets its own isolated workspace.
Key benefits:
- Scalability: One app serves many customers without code duplication.
- Security: Data is strictly isolated; users can't access data from other organizations.
- Flexibility: Users can belong to multiple organizations (e.g., personal and work accounts).
Without proper tenancy, you'd risk data leaks or complex workarounds. The boilerplate uses organization-scoped queries and session-based access control to enforce isolation.
How Organizations Work
User Membership and Active Organization
A user can join multiple organizations through memberships. Each membership has a role (owner, admin, member) that determines permissions.
The "active organization" is stored in the user's session. This shapes what data they see and what actions they can perform. For example:
- When viewing leads, only those from the active organization appear.
- API calls automatically filter by the active organization's ID.
If a user isn't in any organization, they're prompted to create or join one during onboarding.
Data Isolation
All business data (leads, submissions, webhooks, etc.) belongs to an organization. Database queries always include organizationId filters. This ensures:
- Users only see their organization's data.
- Admins can't accidentally access other tenants' info.
- API keys and integrations are organization-specific.
Data Model Overview
The organization system revolves around these core entities. Let's break down the key models from prisma/schema.prisma:
Prop
Type
Organization Model Details
The Organization model is the heart of tenancy:
Prop
Type
Member Model Details
Members connect users to organizations:
Prop
Type
Invitation Model Details
For inviting new members:
Prop
Type
Roles and Permissions
Roles control what users can do within an organization. The boilerplate uses a simple hierarchy:
Prop
Type
Implementing Role Checks
Permissions are enforced in procedures (server-side functions). For example:
// In a procedure
const session = await context.auth.getSession({
requirements: 'authenticated',
roles: ['admin', 'owner'], // Require specific roles
});
// Only admins/owners can proceedGuidelines for roles:
- Least privilege: Default to "member" for new users.
- Explicit checks: Always verify roles for sensitive operations.
- Avoid sprawl: Keep roles simple; use metadata for fine-grained permissions if needed.
API Scoping and Controllers
All API endpoints respect organization boundaries. Controllers read the active organization from the session and filter queries accordingly.
Example: Creating an Organization
Set Up the Controller
The OrganizationController handles org lifecycle. It uses procedures for auth and notifications.
export const OrganizationController = igniter.controller({
name: 'Organization',
path: '/organization',
actions: {
create: igniter.mutation({
// ... config
handler: async ({ request, response, context }) => {
const session = await context.auth.getSession({ requirements: 'authenticated' });
// Create org and set as active
const org = await context.organization.create({
name: request.body.name,
slug: request.body.slug,
userId: session.user.id,
});
return response.created(org);
},
}),
// ... other actions
},
});Filter by Organization
In queries, always include organizationId:
// Get leads for active org
const leads = await context.lead.list({
organizationId: session.organization.id,
});Validate Membership
Before actions, ensure the user belongs to the org:
if (!session.organization) {
return response.forbidden('No active organization');
}Key Endpoints
From the OpenAPI spec:
POST /organization: Create new org.GET /organization/stats: Get org analytics.PUT /organization: Update org details.POST /invitation: Invite members.GET /membership: List org members.
All require authentication and active organization.
Practical Examples
Onboarding a New User
- User signs up → No organization yet.
- Prompt to create org:
POST /organizationwith name/slug. - Auto-set as active org in session.
- User can now add members, resources.
Switching Organizations
- User has multiple memberships.
- Call
POST /auth/set-active-organizationwith org ID. - Session updates; UI refreshes with new org's data.
Inviting Members
- Admin calls
POST /invitationwith email/role. - Invitee receives email with accept link.
- On accept:
POST /invitation/:id/accept→ Creates membership.