.do
Enterprise

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 organization

The platform:

  1. Identifies the current organization from request context
  2. Scopes all queries automatically
  3. Enforces data isolation at the database level
  4. Applies per-organization quotas and limits
  5. Tracks usage and billing per tenant
  6. 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 level

Database-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 organization

API-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' files

Resource 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 projects

Usage 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 impact

Security 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 SQL

2. 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 organization

Summary

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.