Deployment Models
ThreatWeaver supports two deployment models with fundamentally different database architectures. The application behavior is controlled by the DEPLOYMENT_MODE environment variable.
Overviewβ
Model 1: Dedicated (On-Premises)β
When to use: Single customer deployment β on-premises, private cloud, or dedicated managed instance.
Environment variable: DEPLOYMENT_MODE=dedicated
Database Architectureβ
| Aspect | Detail |
|---|---|
| Schema | All tables in public schema |
| Tenant isolation | Not applicable β single tenant |
| Data location | Customer's own infrastructure |
| Migrations | npm run migrate:local (synchronize mode) |
| Tenant slug | Set via DEDICATED_TENANT_SLUG env var (e.g., acme-corp) |
How It Worksβ
- All queries run against the
publicschema - No schema switching overhead
- The
DEDICATED_TENANT_SLUGis used for branding and configuration (e.g., displaying the company name) but does NOT create a separate schema - Simplest deployment β standard PostgreSQL, no multi-tenant complexity
Setupβ
# .env for on-premises deployment
DEPLOYMENT_MODE=dedicated
DEDICATED_TENANT_SLUG=acme-corp
DATABASE_URL=postgresql://user:pass@localhost:5432/threatweaver
# Run migrations (synchronize mode β creates/updates all tables in public schema)
npm run migrate:local
Model 2: SaaS (Multi-Tenant)β
When to use: Hosting multiple customers on a single ThreatWeaver instance β the SaaS offering.
Environment variable: DEPLOYMENT_MODE=saas
Database Architectureβ
| Aspect | Detail |
|---|---|
| Schema strategy | Schema-per-tenant (tenant_acme, tenant_globex, etc.) |
public schema role | Seed templates, default data, tenant registry, new schema creation |
| Tenant isolation | Complete β each tenant's data in its own PostgreSQL schema |
| Migrations | npm run migrate:dev (migration-based) or npm run migrate:production (transaction-safe) |
| Tenant creation | API endpoint provisions new schema from public template |
How It Worksβ
Schema Isolation Detailsβ
Every tenant gets their own schema with a complete copy of all tables (133 entities). The SchemaManager class handles:
- Schema creation:
CREATE SCHEMA IF NOT EXISTS "tenant_acme"+ DDL from template - Request scoping:
SET search_path TO "tenant_acme", publicβ all unqualified table queries resolve to tenant schema - RLS (Row-Level Security):
app.current_tenant_idGUC set at session level for defense-in-depth - Connection pool safety: Schema and tenant GUC reset before returning connections to the pool β prevents context bleed between tenants
- Validation: Schema names validated with regex
/^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/to prevent SQL injection
What Lives in public Schema (SaaS mode)β
| Purpose | Tables/Data |
|---|---|
| Tenant registry | Tenant metadata, subscription status, entitlements |
| DDL templates | Table creation SQL used to seed new tenant schemas |
| Default data | Default scanner agents (50 agents), default scan templates, compliance frameworks |
| Shared configuration | Platform-level settings, AI provider configs |
| Migration tracking | TypeORM migration history |
New Tenant Provisioning Flowβ
Setupβ
# .env for SaaS deployment
DEPLOYMENT_MODE=saas
DATABASE_URL=postgresql://user:pass@supabase-host:5432/threatweaver
# Run migration-based migrations (not synchronize)
npm run migrate:dev # for development/staging
npm run migrate:production # for production (transaction-safe)
Comparison Tableβ
| Feature | Dedicated (On-Premises) | SaaS (Multi-Tenant) |
|---|---|---|
DEPLOYMENT_MODE | dedicated | saas |
| Customers per instance | 1 | Unlimited |
| Database schema | Single public | Schema-per-tenant |
| Data isolation | Physical (separate DB) | Logical (separate schema) |
| Migration strategy | synchronize mode | Migration files |
public schema usage | All application data | Templates + tenant registry |
| Tenant slug source | DEDICATED_TENANT_SLUG env var | JWT tenantSlug claim |
| Search path | Always public | tenant_{slug}, public |
| RLS policies | Not needed | Active (defense-in-depth) |
| Connection pool | Standard | Per-request schema switching with reset |
| Infrastructure | Customer-managed | BluCypher-managed (Render + Supabase) |
| Cost model | License fee | Per-tenant subscription |
Key Source Filesβ
| File | Purpose |
|---|---|
backend/src/multi-tenant/schema-manager.ts | Schema creation, QueryRunner scoping, pool safety |
backend/src/multi-tenant/tenant-local-storage.ts | AsyncLocalStorage-based tenant context |
backend/src/multi-tenant/rls-setup.ts | Row-Level Security policy configuration |
backend/src/middleware/auth.ts | JWT extraction of tenantId/tenantSlug |
backend/scripts/migrate-local.ts | Dedicated mode migrations (synchronize) |
backend/scripts/migrate-dev.ts | SaaS mode migrations (migration-based) |
backend/scripts/migrate-production.ts | Production migrations (transaction-safe) |
Security Considerationsβ
- Schema name injection: All schema names validated against strict regex before use in
SET search_path - Connection pool bleed: QueryRunner resets
search_pathandapp.current_tenant_idbefore release - Cross-tenant access: RLS policies enforce
app.current_tenant_idmatches row-level tenant markers - Schema drop protection:
dropTenantSchema()refuses to drop anything not prefixed withtenant_ - Audit trail: All tenant provisioning and schema operations logged to
SecurityAuditLog
This page documents the actual implementation in backend/src/multi-tenant/. Last verified against source code.