Optare v1.0 is now available. Get started →
Learn
Multi-Tenancy

Multi-Tenancy

Multi-tenancy is the foundation of B2B SaaS. In Optare, every user belongs to one or more organizations (tenants).

What is Multi-Tenancy?

Instead of having separate databases or deployments for each customer, multi-tenancy allows one application to serve many customers with data isolation.

┌─────────────────────────────────────────────────────────┐
│                    Your Application                      │
├─────────────────────────────────────────────────────────┤
│                                                          │
│   ┌─────────┐   ┌─────────┐   ┌─────────┐               │
│   │ Acme Co │   │ Beta Inc│   │ Gamma   │               │
│   │ (Org 1) │   │ (Org 2) │   │ (Org 3) │               │
│   ├─────────┤   ├─────────┤   ├─────────┤               │
│   │ Users   │   │ Users   │   │ Users   │               │
│   │ Data    │   │ Data    │   │ Data    │               │
│   │ Settings│   │ Settings│   │ Settings│               │
│   └─────────┘   └─────────┘   └─────────┘               │
│                                                          │
└─────────────────────────────────────────────────────────┘

Key Concepts

Organizations

An Organization is a customer account. It has:

  • A unique ID and slug
  • Settings and configuration
  • Members with roles
  • Subscriptions and licenses
interface Organization {
  id: string;           // "org_abc123"
  name: string;         // "Acme Corporation"
  slug: string;         // "acme"
  logoUrl?: string;
  settings: OrgSettings;
}

Members

Members are users who belong to an organization with a specific role.

RolePermissions
OwnerFull control, billing, delete org
AdminManage members, settings
MemberUse the product
GuestLimited read access

The User-Organization Relationship

User (john@gmail.com)
    ├── Member of "Acme Corp" (Admin role)
    └── Member of "Beta Inc" (Member role)

A single user can belong to multiple organizations with different roles in each.

How It Works in Code

Get User's Organizations

import { useOptare } from '@optare/optareid-react';
 
function OrgSwitcher() {
  const { organizations, switchOrganization } = useOptare();
 
  return (
    <select onChange={(e) => switchOrganization(e.target.value)}>
      {organizations.map(org => (
        <option key={org.id} value={org.id}>
          {org.name} ({org.role})
        </option>
      ))}
    </select>
  );
}

Current Organization Context

Tokens include the current organization:

{
  "sub": "user_abc123",
  "org_id": "org_xyz789",
  "org_name": "Acme Corp",
  "role": "admin"
}

Server-Side: Scope Data by Organization

app.get('/api/projects', requireAuth, async (req, res) => {
  const orgId = req.user.organizationId;
  
  // Always filter by organization!
  const projects = await db.projects.findMany({
    where: { organizationId: orgId }
  });
  
  res.json(projects);
});

Organization Isolation

Data Isolation

Each organization's data is isolated. Users can only access data belonging to their current organization.

// ✅ Correct: Always scope queries
await db.users.findMany({ 
  where: { organizationId: currentOrgId } 
});
 
// ❌ Wrong: Never query without org scope
await db.users.findMany(); // Security risk!

Feature Isolation

Organizations can have different feature access based on their subscription:

const { hasLicense } = useOptare();
 
{hasLicense('advanced-analytics') && (
  <AnalyticsDashboard />
)}

Common Patterns

1. Organization Signup Flow

1. User signs up
2. User creates an organization
3. User becomes Owner of that organization
4. User can invite team members

2. Invitation Flow

1. Admin invites user by email
2. Invitee receives email
3. Invitee creates account (or logs in)
4. Invitee is added as Member

3. Organization Switching

1. User is viewing Org A
2. User selects Org B from switcher
3. New tokens issued with Org B context
4. UI refreshes with Org B data

Best Practices

Always Include Organization in Queries

// Every database query should include organizationId
const data = await db.table.findMany({
  where: { 
    organizationId: req.user.organizationId,
    // ... other filters
  }
});

Validate Organization Access on Every Request

async function validateOrgAccess(userId: string, orgId: string) {
  const membership = await db.memberships.findFirst({
    where: { userId, organizationId: orgId }
  });
  
  if (!membership) {
    throw new UnauthorizedError('Not a member of this organization');
  }
  
  return membership;
}

Use Role-Based Access Control

import { requirePermission } from '@optare/rbac';
 
app.delete('/api/users/:id', requireAuth, async (req, res) => {
  await requirePermission(req.user, 'members.remove');
  // Delete user...
});

Next Steps