Skip to Content
Multi-Tenancy

Multi-Tenancy & Deployment Models

Overview

The platform is built as a multi-tenant SaaS from day one, with a parallel enterprise on-prem deployment path. A single codebase, a single set of Docker images, and a single Coolify configuration serve all deployment scenarios — only the target environment and tenant config differ.

Terminology shift: what the requirements document calls “clients” is implemented as tenants throughout the codebase.


Tenant Model

A tenant is the top-level organisational unit. Everything in the system belongs to a tenant.

interface Tenant { id: string; slug: string; // URL-safe identifier: acme-agency name: string; plan: 'free' | 'pro' | 'agency' | 'enterprise'; status: 'active' | 'suspended' | 'trialing'; // LLM provider config allowedProviders: LLMProvider[]; // which providers this tenant can use defaultProvider: LLMProvider; dataPrivacyLevel: 'cloud_ok' | 'local_only'; // local_only = Ollama only // Billing monthlySpendCapUsd: number; currentMonthSpendUsd: number; // Deployment deploymentMode: 'saas' | 'on_prem'; onPremCallbackUrl?: string; // on-prem tenants report back here createdAt: Date; }

Tenant Isolation

Every piece of data in the system carries a tenantId. Isolation is enforced at multiple layers:

1. Database layer

PostgreSQL: Every table has a tenant_id column. A Prisma middleware appends WHERE tenant_id = :tenantId to every query automatically — queries cannot return cross-tenant data.

// Prisma middleware — applied globally db.$use(async (params, next) => { if (params.model && tenantScopedModels.has(params.model)) { params.args.where = { ...params.args.where, tenantId: getCurrentTenantId() }; } return next(params); });

MongoDB: Each tenant has its own MongoDB database: dmagency_<tenantSlug>. Mongoose connections are instantiated per-tenant and cached.

const connections = new Map<string, mongoose.Connection>(); function getTenantDb(tenantSlug: string): mongoose.Connection { if (!connections.has(tenantSlug)) { connections.set(tenantSlug, mongoose.createConnection( `${MONGO_URL}/dmagency_${tenantSlug}` )); } return connections.get(tenantSlug)!; }

2. Queue layer

BullMQ queues are shared per agent role — one queue serves all tenants:

agent__activity-planner agent__blog-writer agent__keyword-researcher ...

This mirrors the rag__ingestion and notifications__{channel} queues already in the codebase. Tenant isolation is enforced at the job payload level: every job carries tenantId, and workers verify it before execution. Per-tenant concurrency caps from agent_configs.max_concurrency are applied via BullMQ’s rate limiter keyed on tenantId.

3. API layer

Every authenticated API request carries the tenant context. The Fastify middleware extracts and validates the tenant from the JWT and attaches it to the request:

fastify.addHook('preHandler', async (request, reply) => { const token = request.headers.authorization?.split(' ')[1]; const payload = verifyJwt(token); request.tenantId = payload.tenantId; request.userId = payload.userId; // All downstream handlers use request.tenantId — never trust body/params for this });

4. LLM adapter layer

Adapter dispatch includes tenantId in the task payload. Callbacks are validated to ensure tenantId in the callback token matches the original dispatch. Cross-tenant callback injection is impossible.

5. Skills isolation

Skill files are stored in MongoDB per tenant (dmagency_<tenantSlug> database). Global skills (platform guides, SOPs) are stored in a shared dmagency_global database and referenced by ID, never copied.


URL Structure

In the SaaS model, tenant context is resolved from the subdomain:

acme-agency.dmagency.io → tenantSlug: acme-agency globex.dmagency.io → tenantSlug: globex

Next.js middleware reads the subdomain from request.headers.host, looks up the tenant, and injects tenantId into the request context before any page or route handler runs.

For enterprise on-prem, the entire app runs at a custom domain (e.g. ai.internal.acmecorp.com) — a single-tenant installation with no subdomain routing needed.


Tenant Config per Agent

Agents are configured per tenant. The same agent role (e.g. copywriter) can have different model preferences, skill assignments, cost caps, and adapter types for different tenants.

interface TenantAgentConfig { tenantId: string; agentRole: AgentRole; adapterType: 'claude' | 'openai' | 'ollama' | 'webhook'; modelId: string; skillIds: string[]; toolNames: ToolName[]; limits: { concurrency: number; timeoutMs: number; maxCostUsdPerTask?: number; }; escalationAfterFailures: number; isActive: boolean; }

Plans & Feature Flags

FeatureFreeProAgencyEnterprise
Tenants / workspaces11Unlimited1 (on-prem)
Campaigns / month320UnlimitedUnlimited
Agent roles available2 (Copywriter, SEO)5All 7All 7 + custom
LLM providersClaude onlyClaude + OpenAIAllAll + custom endpoints
Local-only mode (Ollama)NoNoNoYes
Custom skills520UnlimitedUnlimited
HITL approvalsNoYesYesYes
Integrations (Google, Meta etc.)NoYesYesYes
SSO / SAMLNoNoNoYes
On-prem deploymentNoNoNoYes
SLANone99%99.5%Custom

Feature flags are stored in the tenants table and checked at the API layer — no code paths differ between plans, just gates.


SaaS Deployment (Coolify)

One Coolify installation manages all environments. Tenants share infrastructure but are isolated at the data layer.

Coolify (VPS — Hetzner/DO) ├── Production environment │ ├── next (Next.js dashboard) — port 3000 │ ├── api (Fastify + BullMQ workers) — port 3001 │ ├── postgres (PostgreSQL 16) │ ├── mongo (MongoDB 7) │ ├── redis (Redis 7) │ └── ollama (local LLM — optional) ├── Staging environment │ └── (same stack, separate containers, separate DBs) └── Dev environment └── (same stack, seeded with test tenants)

Deploy flow:

  1. Push to main → GitHub Actions runs tests
  2. Tests pass → GitHub Actions triggers Coolify deploy webhook
  3. Coolify pulls new image, runs health check, zero-downtime swap

Enterprise On-Prem Deployment

The enterprise on-prem model ships the same Docker images to the customer’s own infrastructure. Coolify is installed on the customer’s VPS or VM. No data leaves their network.

What the customer gets

  • Same Coolify-managed stack
  • All Docker images (private registry)
  • docker-compose.enterprise.yml with sane defaults
  • Automated backups via Coolify’s backup integration
  • Update process: pull new images + Coolify re-deploy

Data sovereignty

  • PostgreSQL and MongoDB run on the customer’s infrastructure
  • LLM traffic goes to Ollama running on-prem (no cloud LLM required)
  • All agent callbacks route within the customer’s network
  • No telemetry, no phone-home to our infrastructure

Custom LLM endpoints

Enterprise tenants can configure a custom Ollama base URL pointing to their own GPU server or internal LLM inference endpoint:

OLLAMA_BASE_URL=http://gpu-server.internal:11434

Or a webhook adapter pointing to their own agent runtime:

AGENT_WEBHOOK_URL=http://agent-runner.internal:8080/run

On-prem tenant config

In a single-tenant on-prem installation, tenant routing is bypassed — the app runs as a single-tenant instance. This is controlled by:

DEPLOYMENT_MODE=on_prem SINGLE_TENANT_ID=enterprise-client-slug

The middleware skips subdomain resolution and uses SINGLE_TENANT_ID for all requests.


Tenant Onboarding Flow

SaaS self-serve

  1. User signs up → tenant record created (slug, plan: trialing)
  2. Onboarding wizard: connect LLM providers, configure first agent, upload brand voice skill
  3. First campaign submitted → trial started
  4. After 14 days or on payment → plan activated

Enterprise

  1. Provisioned by ops: POST /admin/tenants with config
  2. Coolify deployed to customer infrastructure
  3. Tenant config migrated to on-prem instance
  4. Customer’s IT team manages from there

Admin Panel (Super-Admin)

Beyond the per-tenant dashboard, a super-admin panel (accessible only by the platform team) manages:

  • All tenants: create, suspend, change plan, view usage
  • Global skill library: platform-wide SOPs and guides available to all tenants
  • LLM provider status: which providers are healthy
  • Platform-wide cost overview: total spend across all tenants
  • Deployment management: which tenants are on which Coolify environment

© 2026 Leadmetrics — Internal use only