Skip to main content

Multi-tenant

Krios is multi-tenant from day one. Every business table except tenants carries tenantId; audit_logs carries tenantId too, but its projectId is optional (null for tenant-level actions). The scoped Prisma client filters every query so cross-tenant access is not expressible.

Tenant model

model Tenant {
id String
name String
slug String @unique
plan String
settings Json
databaseUrl String? // V3: encrypted, dedicated DB connection
isActive Boolean
}

A tenant owns:

  • Projects (each with its own schema, environments, sites, content)
  • Users (per-tenant — a user belongs to one tenant)
  • Roles (tenant-scoped role definitions)
  • Audit log entries

Super-admins

Set User.isSuperAdmin = true to grant access to platform-level surfaces. Super-admins see /admin/platform; everyone else is redirected to /admin.

Super-admin is platform-level only — it doesn't elevate permissions inside any specific project. A super-admin still needs a role on a project to author content there.

Physical isolation

By default tenants share the same Postgres database; the scoping middleware keeps them logically isolated. For enterprise customers with regulatory requirements:

  • Set Tenant.databaseUrl to a dedicated Postgres connection string.
  • Stored encrypted with AES-256-GCM under KRIOS_TENANT_DB_KEY.
  • Per-tenant PrismaClient instances are cached process-wide; idle tenants pay nothing.
  • Eviction is automatic on URL change (or via evictTenantPrismaClient).

Migration plan via the CLI:

krios tenant migrate --tenant acme --from shared --to dedicated --db-url postgres://...

Surfaces the migration steps (snapshot → restore → verify → swap → evict) so the platform team can coordinate. The actual swap happens via PUT /api/v1/platform/tenants/{slug} with the new URL; the encryption is automatic.

Tenant management UI

/admin/platform has:

  • Tenants list — every tenant with project count, user count, plan, DB mode
  • Tenant detail — projects, users, stats, DB indicator
  • Create tenant — name, slug, plan, admin email; auto-provisions default roles + first admin user. Returns the temporary password ONCE — relay it through a secure channel.

REST surface (super-admin only):

GET /api/v1/platform/tenants
POST /api/v1/platform/tenants
GET /api/v1/platform/tenants/{slug}
PUT /api/v1/platform/tenants/{slug}
DELETE /api/v1/platform/tenants/{slug} # soft-delete

Governance intelligence

Detects content + schema issues across a project. The analyzer runs eight detectors and caches results in GovernanceIssue:

  • skipped_workflow — published without going through the assigned workflow
  • stale_content — published entries not updated in 90+ days
  • aging_draft — drafts older than 30 days with no published locale
  • broken_ref — published entry references a deleted target
  • unpublished_ref — published entry references an unpublished target
  • translation_gap — published entry missing supported-locale translations
  • orphaned_media — assets not referenced by any entry
  • unused_content_type — type with zero non-deleted entries

Run via the admin UI's Reports → Governance tab or the API:

POST /api/v1/projects/{slug}/governance/analyze
GET /api/v1/projects/{slug}/governance/issues?severity=high
POST /api/v1/projects/{slug}/governance/issues/{id}/resolve

Resolve marks an issue acknowledged; it doesn't fix the underlying problem. A resolution persists across re-runs — refreshAnalysis upserts by signature and never clears resolvedAt, so an issue that fires again after being resolved stays resolved (re-firing does not reopen it).

IP allowlisting (V3)

Project-level gate on the management API:

  • IpAllowlist rows hold CIDR / single-IP entries
  • Project.settings.ipAllowlist.enabled = true activates enforcement
  • Localhost (127.0.0.1, ::1) always permitted so dev workflows don't lock out
  • Self-lockout guard: enabling won't proceed if the caller's IP isn't already in the allowlist

Delivery API (/api/delivery/*, /api/graphql/*) is never affected. Optional applyToAdmin extends gate to the admin UI.

Data residency

Project.dataRegion records the region selected at infrastructure provisioning time. Surfaced via:

  • Settings → Security tab in the admin UI
  • X-Krios-Data-Region header on /api/v1 responses when a region is configured — the project's dataRegion if set, otherwise the KRIOS_DATA_REGION deployment default

The geography is determined by Neon (database) and Supabase (storage) at provisioning. Krios doesn't route traffic; the region is informational.