Multi-Tenancy

cPod is a multi-tenant platform. Every entity in every domain carries a tenantId. The platform enforces tenancy at the database query layer — SDK consumers never pass tenant identifiers.


Core Principle

You never pass tenantId to any SDK call. The platform extracts it from your JWT and stamps it server-side on every read, write, and query.

Developer writes:              Backend executes:

sdk.people.list()         →    db.people.find({
                                 tenantId: 'tnt-abc123',   ← from JWT
                                 deletedAt: null,
                               })

sdk.storage.db.set({      →    mongo.set({
  tier: 'private',               key: 'tnt-abc123/apps/app-myservice/config',
  path: 'config',                value: { ... },
  value: { ... },              })
})

The tenantId in the JWT claim was set by the CoreSDK Control Plane at token issuance, from the app’s registered tenant at registration time. A client cannot modify it.


Isolation Boundaries

┌─────────────────────────────────────────────────────────────────┐
│  Tenant A  (tnt-abc123)                                         │
│                                                                 │
│  ┌────────────────────┐    ┌────────────────────┐               │
│  │   App 1            │    │   App 2            │               │
│  │   app-hr-portal    │    │   app-sec-scanner  │               │
│  │                    │    │                    │               │
│  │  private storage   │    │  private storage   │               │
│  │  (only this app)   │    │  (only this app)   │               │
│  └────────┬───────────┘    └────────┬───────────┘               │
│           │                         │                           │
│           └─────────┬───────────────┘                           │
│                     │                                           │
│              shared storage                                     │
│              (all apps in tenant)                               │
│                                                                 │
│  EDM entities: people, assets, risk, ...                        │
│  (accessible by any app with edm.read scope)                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  Tenant B  (tnt-xyz789)                                         │
│  ... completely isolated from Tenant A at query level ...       │
└─────────────────────────────────────────────────────────────────┘

Organization vs Tenant

The platform uses two terms for the same concept depending on context:

TermContextDescription
TenantPlatform internals, JWT claims, database keysThe isolation boundary at platform level. Prefixed tnt-. Never shown to end users.
OrganizationSDK API, developer-facing docsThe same concept — what enterprise developers think of as “their org”.

The SDK exposes sdk.organizations.getCurrent(), not sdk.tenants.getCurrent(). The response includes your organization’s name, slug, plan, and settings — but internally it maps 1:1 to a Tenant record.

const org = await sdk.organizations.getCurrent()
// {
//   id: 'tnt-abc123',          ← internal tenantId format
//   name: 'Acme Corp',
//   slug: 'acme-corp',
//   plan: 'enterprise',
//   region: 'us-east-1',
//   createdAt: '2025-03-01T00:00:00Z',
// }

You will see tnt- prefixed IDs in audit logs and debug output. This is the same entity as your Organization — the prefix indicates the internal Tenant record.


App Isolation Within a Tenant

Multiple apps can be registered under a single tenant. They share EDM data (subject to scope) but are isolated at the storage layer.

What’s sharedWhat’s isolated
EDM entities (people, assets, risk, …)private storage tier
shared storage tieruser storage is per-user-per-app
Audit log (tenant-scoped)OAuth credentials (client_id, client_secret)

The appId is derived from the OAuth client_id at registration and is stamped into the JWT at issuance. An app cannot self-report a different appId — it is always set from the registered credential.


Rego Policy for Tenant Isolation

The CoreSDK sidecar evaluates a Rego policy on every request. The default policy enforces:

# Simplified example of tenant isolation rules
package cpod.authz

default allow := false

# Allow read if tenant_id in token matches entity tenant_id
allow if {
  input.action == "read"
  input.claims.tenant_id == input.resource.tenant_id
  "edm.read" in input.claims.scopes
}

# Allow write if tenant_id matches and write scope present
allow if {
  input.action in {"create", "update", "delete"}
  input.claims.tenant_id == input.resource.tenant_id
  "edm.write" in input.claims.scopes
}

Tenants can upload custom Rego bundles to extend these rules — for example, to enforce that only users in a specific group can write to certain entity types.


Cross-Tenant Access

Cross-tenant access is not supported in the standard SDK. The platform does not issue tokens that span multiple tenants. If your use case requires data aggregation across tenants (e.g., a multi-org dashboard), use the admin API with appropriate CPOD_ADMIN_TOKEN credentials — this flow is documented in the Management section.


Tenant Provisioning

Tenants are created through the platform admin API, not through the SDK. Once provisioned:

  1. The tenant receives a tnt- prefixed ID
  2. An initial admin user is seeded
  3. The admin registers apps via POST /v1/oauth/apps
  4. Each app registration returns a client_id and one-time client_secret
  5. Apps configure CPOD_CLIENT_ID and CPOD_CLIENT_SECRET and start making SDK calls

From that point on, all tenancy is automatic — no tenantId is ever passed in SDK calls.