Neutrino Docs
Concepts

Multi-Tenancy

How Neutrino isolates resources across platforms and tenants using naming conventions and per-platform provisioning.

Multi-Tenancy

Neutrino is built for multi-tenant applications from the ground up. Every construct — from DNS hostnames to Cloudflare resource names to session cookies — is designed so that multiple platforms can run on shared infrastructure without any data leakage between them.

Platform = your product

A platform is the top-level container in Neutrino. It represents one client product or application. Each platform is identified by a 10-character nano-ID (e.g. k3m9p2xw7q) that appears in every resource name and URL belonging to it.

When Neutrino onboards a new platform, it provisions a completely independent set of Cloudflare resources for that platform. Two platforms on the same Neutrino instance never share a database, auth service, or Workers — they share only the underlying Cloudflare account (Phase 1) and the Neutrino-operated gateway and registry.

Resource isolation by naming

Neutrino enforces isolation through resource naming rather than separate Cloudflare accounts (in Phase 1). Every Cloudflare resource name encodes the platform ID:

{platformId}-default-auth           ← auth Worker
{platformId}-default-auth-db        ← auth D1 database
{platformId}-{stackId}-db           ← stack shared D1
{platformId}-{stackId}-storage      ← stack shared R2
{platformId}-{stackId}-kv           ← stack shared KV

Because all resource names are unique by construction, no cross-platform access is possible at the Cloudflare layer. The registry tracks the mapping from logical names to actual Cloudflare resource IDs.

Tenants within a platform

Within a platform, tenants provide logical isolation. Tenants map directly to Better Auth organisations and represent distinct business units, customer accounts, or teams. A user can belong to multiple tenants with different roles in each.

Sub-tenants nest under tenants for deeper hierarchies when needed.

The active tenant drives the shell context — ShellContext.tenant reflects which tenant the user is currently operating in. Feature permissions are evaluated per-tenant, not per-platform.

Shared Neutrino infrastructure

Some services are operated once by Neutrino and shared across all platforms:

Shared serviceIsolation boundary
GatewayRouting is per-platform by API key / auth token
RegistryEach record belongs to exactly one platform ID
BillingStripe customer per platform
IAMAPI keys scoped to a platform ID

These services never return data across platform boundaries. The registry enforces platform-scoped queries at the query level.

Important: D1 does NOT enforce row-level security. Cross-platform isolation is application-level discipline — every query that touches a tenanted table must include the platformId (or equivalent) in its WHERE clause. The Drizzle schema doesn't enforce this; it's a contract maintained by code review and the standard query helpers in services/{iam,registry}/src/.

DNS and session scope

Every resource for a platform lives under *.{platformId}.nno.app:

auth.svc.default.k3m9p2xw7q.nno.app              ← auth Worker
dashboard.app.x7y8z9w0q1.k3m9p2xw7q.nno.app      ← console app in a stack

Auth session cookies use the domain .<platformId>.nno.app. This means a single login gives the user access to all stacks and apps within their platform — no re-authentication when navigating between stacks.

Suspension enforcement window: Gateway caches IAM session validations for 5 minutes (in-memory per Worker isolate). A platform suspension may take up to 5 minutes to fully propagate to running sessions; the Gateway's KV-backed platform:{id}:status lookup (60s TTL) is the faster path for real-time suspension.

Naming utilities

When building features, never construct platform or resource identifiers by hand. Use the utilities from @neutrino-io/core/naming:

import { generateId, buildResourceName } from "@neutrino-io/core/naming";

const platformId = generateId(); // → 'k3m9p2xw7q'
buildResourceName(platformId, "default", "auth"); // → 'k3m9p2xw7q-default-auth'

These functions encode the naming conventions and prevent format drift.

On this page