Multi-Tenancy
Built-in organization isolation, per-tenant data security, and resource management - multi-tenancy is automatic on the .do platform
The .do platform is multi-tenant by default. Every application you build automatically supports multiple isolated organizations without writing tenant isolation code.
How It Works
Multi-tenancy is built into platform primitives:
// You write simple queries
const projects = await $.Project.list()
// Platform automatically:
// - Adds organizationId filter
// - Enforces row-level security
// - Applies resource quotas
// - Tracks per-tenant usage
// - Maintains complete isolation
// Result: Only projects from user's organizationThe platform:
- Identifies the current organization from request context
- Scopes all queries automatically
- Enforces data isolation at the database level
- Applies per-organization quotas and limits
- Tracks usage and billing per tenant
- Isolates configuration and settings
Organization Model
Every user belongs to an organization:
// Organizations are first-class primitives
{
id: 'org_01ABC...',
name: 'Acme Corporation',
// Verified domains
domains: ['acme.com', 'acme.co.uk'],
// Subscription and billing
subscription: {
plan: 'enterprise',
status: 'active',
seats: 100
},
// Per-organization settings
settings: {
branding: {
logo: 'https://acme.com/logo.png',
primaryColor: '#FF6B35'
},
security: {
enforceSSO: true,
sessionTimeout: 3600,
allowedIPs: ['203.0.113.0/24']
},
features: {
advancedAnalytics: true,
apiAccess: true,
customIntegrations: true
}
},
// Timestamps
createdAt: '2025-01-01T00:00:00.000Z',
updatedAt: '2025-01-20T10:00:00.000Z'
}Automatic Data Isolation
All data is automatically scoped to organizations:
// Create data - organizationId added automatically
const project = await $.Project.create({
name: 'Website Redesign',
status: 'active'
})
// Stored as: { name: 'Website Redesign', status: 'active', organizationId: 'org_01ABC...' }
// Query data - scoped automatically
const projects = await $.Project.list()
// Platform adds: WHERE organizationId = 'org_01ABC...'
// Update data - isolation enforced
await $.Project.update(projectId, { status: 'completed' })
// Platform verifies: projectId belongs to current organization
// Delete data - isolation enforced
await $.Project.delete(projectId)
// Platform verifies: projectId belongs to current organization
// No way to access other organizations' data
// Isolation is enforced at the platform levelDatabase-Level Security
The platform enforces isolation at multiple levels:
Row-Level Security (RLS)
// Platform implements PostgreSQL row-level security automatically
// Every table has RLS policies:
// CREATE POLICY org_isolation ON projects
// USING (organization_id = current_setting('app.current_org_id')::uuid)
// On every request, platform sets organization context:
// SET app.current_org_id = 'org_01ABC...'
// All queries are automatically scoped
// Even if your code tries to bypass platform primitives:
const result = await db.execute('SELECT * FROM projects')
// Only returns projects for current organizationAPI-Level Isolation
// API requests automatically include organization context
on($.API.request, async ({ user, request }) => {
// Platform sets organization context
request.organizationId = user.organizationId
// All subsequent operations are scoped
const data = await $.Data.query(request.query)
// Automatically filtered to user's organization
})Storage Isolation
// File storage is automatically namespaced
await $.Storage.upload('logo.png', fileData)
// Stored at: organizations/org_01ABC.../files/logo.png
await $.Storage.get('logo.png')
// Retrieves from: organizations/org_01ABC.../files/logo.png
// Cannot access other organizations' filesResource Quotas
Enforce per-organization limits automatically:
// Define quotas per plan
const quotas = {
free: {
users: 5,
projects: 10,
storage: 1 * GB,
apiCalls: 1000,
customDomains: 0
},
pro: {
users: 50,
projects: 100,
storage: 100 * GB,
apiCalls: 100000,
customDomains: 3
},
enterprise: {
users: Infinity,
projects: Infinity,
storage: Infinity,
apiCalls: Infinity,
customDomains: Infinity
}
}
// Platform enforces automatically
on($.Project.create, async ({ organization }) => {
const quota = quotas[organization.subscription.plan]
const current = await $.Project.count({ organizationId: organization.id })
if (current >= quota.projects) {
throw new Error(`Project limit reached (${quota.projects}). Upgrade to create more.`)
}
})
// Or let platform enforce automatically:
await $.Organization.setQuota(orgId, 'projects', 100)
// Platform prevents creating more than 100 projectsUsage Tracking
Track resource consumption per organization:
// Platform automatically tracks usage
const usage = await $.Organization.getUsage(orgId, {
period: 'current_month'
})
console.log({
apiCalls: usage.apiCalls,
storage: usage.storage,
bandwidth: usage.bandwidth,
computeTime: usage.computeTime,
users: usage.activeUsers,
estimated Cost: usage.estimatedCost
})
// Bill based on usage
on($.Billing.generateInvoice, async ({ organization }) => {
const usage = await $.Organization.getUsage(organization.id, {
period: 'last_month'
})
const charges = calculateCharges(usage, organization.subscription.plan)
await $.Invoice.create({
organizationId: organization.id,
amount: charges.total,
lineItems: charges.items,
period: getLastMonth()
})
})Per-Tenant Configuration
Each organization can have custom settings:
// Set organization-specific configuration
await $.Organization.update(orgId, {
settings: {
branding: {
logo: 'https://cdn.acme.com/logo.png',
primaryColor: '#FF6B35',
customDomain: 'app.acme.com'
},
features: {
advancedAnalytics: true,
apiAccess: true,
webhooks: true
},
integrations: {
slack: {
webhookUrl: 'https://hooks.slack.com/...',
channel: '#engineering'
},
datadog: {
apiKey: 'dd_api_key_xxx',
enabled: true
}
}
}
})
// Access in application code
on($.Event.occurred, async ({ event, organization }) => {
const settings = organization.settings
// Apply custom branding
if (settings.branding?.customDomain) {
// Use custom domain in emails, links, etc.
}
// Check feature flags
if (settings.features?.webhooks) {
await $.Webhook.send(event)
}
// Use integrations
if (settings.integrations?.slack?.enabled) {
await notifySlack(event, settings.integrations.slack)
}
})Organization Hierarchy
Support departments and teams within organizations:
// Create departments
const engineering = await $.Department.create({
name: 'Engineering',
organizationId: orgId,
parentId: null // Top-level
})
const backend = await $.Department.create({
name: 'Backend Team',
organizationId: orgId,
parentId: engineering.id
})
// Assign users to departments
await $.User.assign.Department({
userId: user.id,
departmentId: backend.id
})
// Query by department
const backendUsers = await $.User.list({
departmentId: backend.id
})
// Department-level permissions
on($.API.request, async ({ user, resource }) => {
if (resource.restrictedToDepartment) {
const userDepartment = await $.User.getDepartment(user.id)
if (userDepartment.id !== resource.departmentId) {
throw new Error('Access restricted to specific department')
}
}
})Domain Verification
Organizations can verify ownership of domains:
// Platform provides domain verification automatically
await $.Organization.addDomain(orgId, 'acme.com')
// Platform generates verification token
const verification = await $.Domain.getVerification('acme.com')
console.log({
method: 'dns', // or 'email', 'http'
token: 'verify_abc123xyz',
instructions: 'Add TXT record: _platform-verify=verify_abc123xyz'
})
// After verification
on($.Domain.verified, async ({ organization, domain }) => {
// Enable domain-based features
// - SSO routing
// - Email sending from domain
// - Custom subdomain (app.acme.com)
console.log(`${domain} verified for ${organization.name}`)
})
// Automatically route users by email domain
on($.User.signup, async ({ email }) => {
const domain = email.split('@')[1]
// Platform finds organization by domain
const org = await $.Organization.findByDomain(domain)
if (org) {
// Add user to existing organization
await $.User.create({
email,
organizationId: org.id
})
} else {
// Create new organization
const newOrg = await $.Organization.create({
name: `${domain} Organization`,
domains: [domain]
})
await $.User.create({
email,
organizationId: newOrg.id,
role: 'admin' // First user is admin
})
}
})Cross-Organization Features
Sometimes you need to share data across organizations:
// Public resources (visible to all organizations)
await $.Resource.create({
name: 'Public Template',
type: 'template',
visibility: 'public'
})
// Shared resources (specific organizations)
await $.Resource.share({
resourceId: resource.id,
sharedWith: ['org_01ABC...', 'org_01DEF...'],
permissions: ['read']
})
// Query respects visibility
on($.Resource.list, async ({ organization }) => {
// Returns:
// 1. Resources owned by this organization
// 2. Resources shared with this organization
// 3. Public resources
// Platform handles complex query automatically
})Organization Members
Manage users within organizations:
// Add member
await $.Organization.addMember({
organizationId: orgId,
userId: userId,
role: 'member' // or 'admin', 'owner'
})
// List members
const members = await $.Organization.getMembers(orgId)
// Update member role
await $.Organization.updateMember({
organizationId: orgId,
userId: userId,
role: 'admin'
})
// Remove member
await $.Organization.removeMember({
organizationId: orgId,
userId: userId
})
// Transfer ownership
await $.Organization.transferOwnership({
organizationId: orgId,
newOwnerId: userId
})Billing & Subscriptions
Per-organization billing is automatic:
// Update subscription
await $.Organization.updateSubscription(orgId, {
plan: 'enterprise',
seats: 100,
addOns: ['advanced-analytics', 'priority-support']
})
// Track what each organization uses
const usage = await $.Organization.getUsage(orgId)
// Generate invoices
await $.Invoice.create({
organizationId: orgId,
amount: calculateAmount(usage),
period: getCurrentMonth(),
dueDate: addDays(new Date(), 15)
})
// Handle payment
on($.Payment.received, async ({ organizationId, amount }) => {
await $.Organization.update(organizationId, {
'subscription.status': 'active',
'subscription.paidThrough': addMonths(new Date(), 1)
})
})Performance
Multi-tenancy adds minimal overhead:
// Platform uses efficient indexing
// CREATE INDEX idx_projects_org ON projects(organization_id)
// Queries are fast with proper indexes
const projects = await $.Project.list()
// Execution time: `<10ms` even with millions of total projects
// Organization context is cached
// No repeated database lookups
// Row-level security policies are compiled
// Minimal performance impactSecurity Best Practices
The platform enforces security automatically:
1. Always Use Platform Primitives
// Good: Platform enforces isolation
const projects = await $.Project.list()
// Bad: Raw SQL bypasses safety checks
const projects = await db.execute('SELECT * FROM projects')
// Still safe! RLS policies apply even to raw SQL2. Validate Organization Ownership
// Platform validates automatically
await $.Project.update(projectId, { name: 'New Name' })
// Platform verifies projectId belongs to current organization
// Manual validation (rarely needed)
on($.API.request, async ({ user, resourceId }) => {
const resource = await $.Resource.find(resourceId)
if (resource.organizationId !== user.organizationId) {
throw new Error('Resource not found') // Don't reveal existence
}
})3. Audit Organization Access
// Platform logs all cross-organization attempts
on($.Security.accessDenied, async ({ user, resource, reason }) => {
await $.Alert.send({
type: 'security',
severity: 'high',
message: `${user.email} attempted to access ${resource.type} from different organization`,
metadata: { user, resource, reason }
})
})Testing Multi-Tenancy
Test with multiple organizations easily:
// Create test organizations
const org1 = await $.Organization.create({ name: 'Test Org 1' })
const org2 = await $.Organization.create({ name: 'Test Org 2' })
// Create users in each
const user1 = await $.User.create({ email: '[email protected]', organizationId: org1.id })
const user2 = await $.User.create({ email: '[email protected]', organizationId: org2.id })
// Verify isolation
await $.withUser(user1, async () => {
// Create data as user1
const project = await $.Project.create({ name: 'Org 1 Project' })
// Switch to user2
await $.withUser(user2, async () => {
// Try to access user1's project
const result = await $.Project.find(project.id)
expect(result).toBeNull() // Isolation enforced!
// Can only see user2's organization data
const projects = await $.Project.list()
expect(projects).toHaveLength(0)
})
})Migration
Moving to platform multi-tenancy:
Before (Custom Multi-Tenancy)
// Manual organization filtering everywhere
app.get('/projects', async (req, res) => {
const orgId = req.user.organizationId
const projects = await db.projects.find({ organizationId: orgId })
res.json({ projects })
})
// Easy to forget
app.get('/projects/:id', async (req, res) => {
const project = await db.projects.findById(req.params.id)
// Bug: No organization check! Security vulnerability!
res.json({ project })
})
// Complex joins with organization checks
const projects = await db.execute(`
SELECT p.*, u.name as owner_name
FROM projects p
JOIN users u ON p.owner_id = u.id
WHERE p.organization_id = $1
AND u.organization_id = $1
`, [orgId])After (Platform Multi-Tenancy)
// Organization context automatic
on($.API.request, async ({ path }) => {
if (path === '/projects') {
return await $.Project.list()
// Platform adds organization filter automatically
// Impossible to forget
// No security bugs
}
if (path.startsWith('/projects/')) {
const id = path.split('/')[2]
return await $.Project.find(id)
// Platform verifies organization ownership
// Returns null if not found or wrong organization
}
})
// Joins automatically include organization checks
const projects = await $.Project.list({
include: ['owner']
})
// Platform ensures related records are from same organizationSummary
Multi-tenancy on the .do platform is:
- Automatic - Data isolation without code
- Secure - Database-level enforcement
- Performant - Efficient indexing and caching
- Flexible - Per-tenant configuration and quotas
- Simple - No tenant management code needed
Focus on your features. Let the platform handle multi-tenancy.
Related Documentation
- Authentication - Organization-aware authentication
- User Provisioning - Automatic user management
- Admin Portals - Per-organization configuration
- Compliance - Organization-scoped audit logs
- Database - Data storage primitives
Automatic User Provisioning
Real-time user and group synchronization from enterprise directories via SCIM - no manual onboarding or offboarding required
Self-Service Admin Portals
Built-in admin interfaces for enterprise IT administrators to configure SSO, directory sync, and integrations without developer involvement