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: globexNext.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
| Feature | Free | Pro | Agency | Enterprise |
|---|---|---|---|---|
| Tenants / workspaces | 1 | 1 | Unlimited | 1 (on-prem) |
| Campaigns / month | 3 | 20 | Unlimited | Unlimited |
| Agent roles available | 2 (Copywriter, SEO) | 5 | All 7 | All 7 + custom |
| LLM providers | Claude only | Claude + OpenAI | All | All + custom endpoints |
| Local-only mode (Ollama) | No | No | No | Yes |
| Custom skills | 5 | 20 | Unlimited | Unlimited |
| HITL approvals | No | Yes | Yes | Yes |
| Integrations (Google, Meta etc.) | No | Yes | Yes | Yes |
| SSO / SAML | No | No | No | Yes |
| On-prem deployment | No | No | No | Yes |
| SLA | None | 99% | 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:
- Push to
main→ GitHub Actions runs tests - Tests pass → GitHub Actions triggers Coolify deploy webhook
- 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.ymlwith 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:11434Or a webhook adapter pointing to their own agent runtime:
AGENT_WEBHOOK_URL=http://agent-runner.internal:8080/runOn-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-slugThe middleware skips subdomain resolution and uses SINGLE_TENANT_ID for all requests.
Tenant Onboarding Flow
SaaS self-serve
- User signs up → tenant record created (slug, plan:
trialing) - Onboarding wizard: connect LLM providers, configure first agent, upload brand voice skill
- First campaign submitted → trial started
- After 14 days or on payment → plan activated
Enterprise
- Provisioned by ops:
POST /admin/tenantswith config - Coolify deployed to customer infrastructure
- Tenant config migrated to on-prem instance
- 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