Skip to main content

Context IDs

Context IDs provide request and runtime scoping across the TWIN stack. They are used to carry identity and tenancy metadata through async execution, enforce route-level context rules, and derive storage partition keys.

AsyncLocalStorage​

The context package stores request context in Node.js AsyncLocalStorage through ContextIdStore.

At request entry, the API server creates a mutable contextIds object and runs route processing inside ContextIdStore.run(contextIds, ...). This means all downstream service code can read the same active context with ContextIdStore.getContextIds() without passing context through every function signature.

Engine lifecycle execution also runs inside ContextIdStore.run(...) so startup and shutdown logic can use the configured context IDs consistently.

Context IDs: Async Propagation Through a Request

Incoming Request
▼
API server: ContextIdStore.run(contextIds, handler) — establishes async scope
TenantProcessor
Resolves tenant from x-api-key (or query parameter fallback) and writes contextIds.tenant
AuthHeaderProcessor
Verifies JWT and writes contextIds.organization (from org claim) and contextIds.user (from sub claim)
Route → Component → Connector
Any code anywhere in the call stack calls ContextIdStore.getContextIds() to read the full active context — no parameter threading required

Default Keys and Value Derivation​

The default key names come from ContextIdKeys:

  • node
  • tenant
  • organization
  • user
  • userOrganization

The table below shows where values are populated.

KeyPrimary SourceHow Value Is Derived
nodeNode runtime stateDuring node startup, configureContextIds reads state.nodeId and sets engine.addContextId(ContextIdKeys.Node, state.nodeId) when node identity is enabled.
tenantTenant processor (per request)In multi-tenant mode, TenantProcessor resolves the tenant per request using one of two strategies. For endpoints matching the apiKeyEndpoints pattern (configurable; default /login), it reads the x-api-key header and looks up the tenant by API key. For all other non-skipTenant routes, it reads the organization query parameter and resolves the tenant whose organizationId matches (or falls back to a organizationIdLegacy alias search). On success, it writes the tenant ID to contextIds[ContextIdKeys.Tenant] and tenant.organizationId to contextIds[ContextIdKeys.Organization]. Routes with skipAuth: true do not require the organization param; if absent, neither context ID is set for that request. On auth-protected routes, AuthHeaderProcessor subsequently overrides both values: it reads the tid claim from the verified JWT, looks up that tenant by ID, and overwrites contextIds[ContextIdKeys.Tenant] with the JWT tid and contextIds[ContextIdKeys.Organization] with that tenant's organizationId. The JWT tid is therefore authoritative for authenticated requests.
organizationNode state (single-tenant) or tenant table (multi-tenant)In single-tenant mode, configureContextIds reads state.nodeOrganizationId and registers it as the engine-level organization context ID at startup. SingleTenantProcessor then injects it into every request context, authenticated or not. In multi-tenant mode, TenantProcessor resolves tenant.organizationId from the tenant table per request. AuthHeaderProcessor sets it by looking up the tenant whose ID matches the JWT tid claim and reading that tenant's organizationId. If no organizationId is found on the tenant, it falls back to the nodeOrganizationId cached at startup (which equals state.nodeOrganizationId in single-tenant mode).
userUser entity record (JWT sub)AuthHeaderProcessor verifies the JWT and performs a partitioned user lookup: it scopes the entity storage read to the tenant partition identified by the JWT tid claim, then retrieves the user by the JWT sub value. If the user does not exist in that partition, authentication fails. On success, contextIds[ContextIdKeys.User] is set from user.identity (validated to match the JWT sub).
userOrganizationUser entity record (post-login)AuthHeaderProcessor sets contextIds[ContextIdKeys.UserOrganization] from user.organization in the authenticated user entity record. The JWT org claim is validated against this field (they must match for authentication to succeed), but the context ID is sourced from the entity record, not the JWT directly. Only present on requests where the caller has logged in.

Notes:

  • On auth-protected routes, AuthHeaderProcessor uses the JWT tid claim to run the user lookup inside that tenant's storage partition. If the user does not exist in that partition, the lookup returns nothing and authentication fails: a JWT cannot be used to access a tenant partition the user was not created in.
  • The runtime key is organization in code; this corresponds to organisation scope in platform documentation.
  • userOrganization carries the organisation DID from the authenticated user's own identity record and is distinct from organization, which identifies the whole tenant.

Context ID Handlers and Short Forms​

Context IDs can be transformed by handlers before partitioning.

  • DidContextIdHandler validates DID format and returns the DID id segment as the short value (hex ids are compacted to base64url).
  • TenantIdContextIdHandler validates tenant id format and returns a compact base64url short form.

Engine context-key registration associates each key with required handler features, then binds the matching handler implementation into ContextIdHandlerFactory.

Handlers are registered only for keys intended for partitioning. The organization, user, and userOrganization keys are intentionally left without default handlers. The organization key identifies the whole tenant organisation, making it equivalent to the tenant partition boundary, so a separate handler would produce redundant isolation. The user and userOrganization keys are only available after login and are absent during background tasks and pre-login flows, making them unsuitable as universal partition keys.

Full and Short Form Examples​

The table below shows illustrative full values and their short forms as used for partitioning.

KeyExample Full FormExample Short FormHow Short Form Is Produced
nodedid:iota:0x8c3f4a5e8d9a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0ajD9KXo2ae2xdTj8qGwydjn9qW0w9Lh8KDidContextIdHandler takes the DID id segment and compacts hex to base64url.
tenant7f3a9c1e0b4d6f8a2c5e7b9d1a3c4e6ffzqcHgtNb4osXnudGjxObwTenantIdContextIdHandler converts 32-hex tenant id to base64url.
organizationdid:iota:0x1234567890abcdef1234567890abcdef1234567890abcdefNot producedNo default context-id handler is registered for this key.
userdid:iota:0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefNot producedNo default context-id handler is registered for this key.
userOrganizationdid:iota:0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeNot producedNo default context-id handler is registered for this key.

When multiple keys are used for partitioning, ContextIdHelper.combinedContextKey(...) joins the short forms with /. For example:

  • Keys: node, tenant
  • Combined partition key: jD9KXo2ae2xdTj8qGwydjn9qW0w9Lh8K/fzqcHgtNb4osXnudGjxObw

Data Partitioning​

Many connectors support context-based partitioning through optional partitionContextIds settings. When configured, the connector:

  1. reads current context IDs using ContextIdStore.getContextIds(),
  2. derives a combined partition key via ContextIdHelper.combinedContextKey(...),
  3. uses that key to isolate stored data.

Examples:

  • Entity storage connectors persist an internal partitionId and include it in get, set, remove, and query filtering.
  • Blob storage connectors compose keys as <partitionKey>/<id>, with a root fallback where relevant.

Partition Key Derivation

node (full form)
did:iota:0x8c3f4a5e...1f0a
→
DidContextIdHandler
extracts id segment, compacts hex to base64url
→
node (short form)
jD9KXo2ae2x...
tenant (full form)
7f3a9c1e0b4d6f8a...4e6f
→
TenantIdContextIdHandler
converts 32-hex tenant id to base64url
→
tenant (short form)
fzqcHgtNb4...
▼ ContextIdHelper.combinedContextKey(...) joins short forms with /
Combined Partition Key
jD9KXo2ae2x.../fzqcHgtNb4...
Used by connectors to isolate stored data by partition — entity storage includes it as a filter, blob storage prefixes keys with it

Partition Key Strategy​

In a single-tenant deployment, node alone is the natural partition key. The tenant context ID is absent, so ContextIdHelper.pickKeysFromAvailable filters it out automatically.

In a multi-tenant deployment, node + tenant is the recommended default for all tenant-owned resources. This gives each tenant complete isolation within a shared database and is the configuration used by default for entity storage, blob storage, logging, and all other standard services.

The only exception is genuinely shared resources: storage that is intentionally accessible across tenants or shared between nodes. The Federated Catalogue is the primary example: its data represents cross-organisational catalogue entries that must be visible to multiple parties, so it is partitioned by node alone (no tenant in the key).

Shared-resource storage must not use tenant in the partition key, but this removes the automatic isolation that tenancy provides. These services compensate by enforcing caller identity at the operation level using trust verification. The ownerId from ITrustVerificationInfo.identity (the verified caller DID extracted from the trust payload) is checked before any write or sensitive read, ensuring that only authorised callers can modify records even though the storage itself is not tenant-partitioned.

The rule of thumb is: if data belongs to one tenant, partition by node + tenant; if data is shared across tenants or nodes, partition by node only and guard writes with trust verification.

Optional Partition IDs​

Partition IDs are optional by design:

  • If partitionContextIds is not configured, no context partition key is produced.
  • If no partition key is produced, storage operates in a non-partitioned scope.
  • If partition keys are configured but required context IDs are missing, context guards fail early rather than silently crossing partitions.

This model supports both single-scope deployments and strict multi-scope isolation without changing application service contracts.

For details on how partitioning behaves across different storage backends, multi-tenant deployment models, and tenant management, see Data Partitioning and Multi-Tenant Architecture.