تخطَّ إلى المحتوى

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.

ConventionColumnTypeWho uses it
Multi-App Isolationsource_account_idTEXT NOT NULL DEFAULT 'primary'Plugin authors, SDK middleware
Cloud Multi-Tenancytenant_idUUIDnself tenant CLI, billing, cost metering

These are not interchangeable.

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"
}
}

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"}}

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)
ForbiddenWhy
Use source_account_id to separate paying Cloud customersTEXT slug is not UUID-safe; no billing integration; violates PatternTenantScoped
Use tenant_id for multi-app isolation within one deployUUID 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: trueMulti-app deploys get no isolation for that table
Add tenant_id to any table without a Hasura row filterCross-tenant data visible via GraphQL — immediate data leak

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.

nself doctor --deep runs check PERM-RLS-01 on every deployment. It verifies:

  1. Every np_* table has RLS enabled and at least one policy.
  2. Every table with multiApp.supported: true has FORCE RLS active (relforcerowsecurity = true).
  3. Every table with a tenant_id column has a Hasura select permission with a tenant_id row filter for the user role.

Violations exit non-zero and print structured failure messages:

RLS-FORCE-MISSING table=np_chat_messages role=user
HASURA-FILTER-MISSING table=np_claw_cost_events role=user

Use --strict to escalate warnings to errors. This check runs without a license key.

All np_* tables across the 40+ plugins that declare multiApp.supported: true. Key examples:

  • np_auditlog_events — audit-log plugin
  • np_chat_conversations, np_chat_messages — chat plugin
  • np_claw_conversations, np_claw_messages — claw plugin
  • np_notify_events — notify plugin

Full list: plugins-pro/registry.json field multiApp.supported.

Introduced in the S74 Cloud tenancy work. As of v1.1.0:

  • np_claw_profile_ab_tests
  • np_ai_usage_rollup
  • np_claw_cost_events
  • np_auditlog_events — nullable tenant_id added for forward-compat

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.