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.databaseUrlto a dedicated Postgres connection string. - Stored encrypted with AES-256-GCM under
KRIOS_TENANT_DB_KEY. - Per-tenant
PrismaClientinstances 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 workflowstale_content— published entries not updated in 90+ daysaging_draft— drafts older than 30 days with no published localebroken_ref— published entry references a deleted targetunpublished_ref— published entry references an unpublished targettranslation_gap— published entry missing supported-locale translationsorphaned_media— assets not referenced by any entryunused_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:
IpAllowlistrows hold CIDR / single-IP entriesProject.settings.ipAllowlist.enabled = trueactivates 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-Regionheader on/api/v1responses when a region is configured — the project'sdataRegionif set, otherwise theKRIOS_DATA_REGIONdeployment default
The geography is determined by Neon (database) and Supabase (storage) at provisioning. Krios doesn't route traffic; the region is informational.