Multi-Tenancy Conventions
هذا المحتوى غير متوفر بلغتك بعد.
Multi-Tenancy Conventions
Section titled “Multi-Tenancy Conventions”ɳSelf ships two distinct multi-tenancy mechanisms. They look similar at a glance but solve different problems. Mixing them causes silent data leaks across paying customers.
The two mechanisms
Section titled “The two mechanisms”| Convention | Column | Type | Who uses it |
|---|---|---|---|
| Multi-App Isolation | source_account_id | TEXT NOT NULL DEFAULT 'primary' | Plugin authors, SDK middleware |
| Cloud Multi-Tenancy | tenant_id | UUID | nself tenant CLI, billing, cost metering |
These are not interchangeable.
Multi-App Isolation: source_account_id
Section titled “Multi-App Isolation: source_account_id”source_account_id TEXT NOT NULL DEFAULT 'primary' appears in every np_* database table that declares multiApp.supported: true in its plugin.json.
It separates independent consumer apps within a single ɳSelf deployment.
Example: a self-hosted ɳSelf stack serving both a Flock app and a Veterans app. Each app’s data is invisible to the other. The column is a human-readable slug: 'primary', 'unity-flock', 'unity-veterans'.
Single-user deployments never set this column. The DEFAULT 'primary' means all rows belong to the default account, transparently.
The middleware enforces isolation via the X-Source-Account HTTP header on every request.
RLS pattern (PatternUserOwned):
CREATE POLICY np_chat_select ON np_chat_conversations FOR SELECT USING ( source_account_id = current_setting('app.source_account_id', true) AND user_id = current_setting('app.user_id', true) );Plugin manifest declaration — every plugin that isolates by source_account_id must declare:
{ "multiApp": { "supported": true, "isolation_column": "source_account_id" }}Cloud Multi-Tenancy: tenant_id
Section titled “Cloud Multi-Tenancy: tenant_id”tenant_id UUID appears in tables that track usage, cost, and plan-level data for operators running ɳSelf Cloud at scale: np_claw_profile_ab_tests, np_ai_usage_rollup, np_claw_cost_events.
It separates paying customers of the Cloud operator.
Example: an ɳSelf Cloud operator serving acme-corp and beta-ltd as two independent customers. Each customer’s cost events, AI usage rollups, and billing records are scoped by their UUID, created via nself tenant create.
RLS pattern (PatternTenantScoped):
CREATE POLICY np_ai_usage_select ON np_ai_usage_rollup FOR SELECT USING (tenant_id = current_setting('app.tenant_id', true)::uuid);Required Hasura row filter — every np_* table with a tenant_id column must have a Hasura metadata permission entry with this select row filter for the user role:
{"tenant_id": {"_eq": "X-Hasura-Tenant-Id"}}Which to use
Section titled “Which to use”Use this decision tree when adding a new database column:
I need to store data per: ├─ App-within-a-deploy (e.g. Flock vs Veterans data in one stack) │ └─ source_account_id TEXT NOT NULL DEFAULT 'primary' │ Declare multiApp.supported: true in plugin.json │ RLS: PatternUserOwned or PatternPublic │ └─ Cloud customer of the operator (e.g. acme-corp vs beta-ltd) └─ tenant_id UUID (nullable — not all deploys are Cloud) Add Hasura row filter: {"tenant_id": {"_eq": "X-Hasura-Tenant-Id"}} RLS: PatternTenantScoped Backfill plan: NULL for existing rows (single-user safe)What must never happen
Section titled “What must never happen”| Forbidden | Why |
|---|---|
Use source_account_id to separate paying Cloud customers | TEXT slug is not UUID-safe; no billing integration; violates PatternTenantScoped |
Use tenant_id for multi-app isolation within one deploy | UUID is not ergonomic for slug-based app identity; billing metering attaches to the wrong surface |
Omit source_account_id from a table where multiApp.supported: true | Multi-app deploys get no isolation for that table |
Add tenant_id to any table without a Hasura row filter | Cross-tenant data visible via GraphQL — immediate data leak |
PR requirements
Section titled “PR requirements”Every PR that adds source_account_id or tenant_id to a new table must include a sentence in the PR description explaining which convention applies and why, based on the decision tree above. PRs without this justification are blocked in code review.
Enforcement: nself doctor --deep
Section titled “Enforcement: nself doctor --deep”nself doctor --deep runs check PERM-RLS-01 on every deployment. It verifies:
- Every
np_*table has RLS enabled and at least one policy. - Every table with
multiApp.supported: truehas FORCE RLS active (relforcerowsecurity = true). - Every table with a
tenant_idcolumn has a Hasuraselectpermission with atenant_idrow filter for theuserrole.
Violations exit non-zero and print structured failure messages:
RLS-FORCE-MISSING table=np_chat_messages role=userHASURA-FILTER-MISSING table=np_claw_cost_events role=userUse --strict to escalate warnings to errors. This check runs without a license key.
Tables by convention (v1.1.0)
Section titled “Tables by convention (v1.1.0)”Using source_account_id
Section titled “Using source_account_id”All np_* tables across the 40+ plugins that declare multiApp.supported: true. Key examples:
np_auditlog_events— audit-log pluginnp_chat_conversations,np_chat_messages— chat pluginnp_claw_conversations,np_claw_messages— claw pluginnp_notify_events— notify plugin
Full list: plugins-pro/registry.json field multiApp.supported.
Using tenant_id
Section titled “Using tenant_id”Introduced in the S74 Cloud tenancy work. As of v1.1.0:
np_claw_profile_ab_testsnp_ai_usage_rollupnp_claw_cost_eventsnp_auditlog_events— nullabletenant_idadded for forward-compat
nself tenant CLI
Section titled “nself tenant CLI”The nself tenant CLI (create, upgrade, suspend, destroy, audit) operates at the tenant_id data and RLS layer. Provisioning automation, Stripe billing integration, license revocation on destroy, and runtime suspension are available in v1.1.0. Review the nself tenant command reference before using this in production paying-customer onboarding flows.
See nself tenant command reference for details.
See also
Section titled “See also”- Multi-Tenant Isolation Models — choosing row-level vs schema vs database-per-tenant for your own B2B SaaS
- nself doctor command reference — full list of PERM-RLS-* checks
cli/internal/database/rls_tenant.go—PatternUserOwned,PatternPublic,PatternTenantScopedimplementations