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
Default Keys and Value Derivation
The default key names come from ContextIdKeys:
nodetenantorganizationuseruserOrganization
The table below shows where values are populated.
| Key | Primary Source | How Value Is Derived |
|---|---|---|
node | Node runtime state | During node startup, configureContextIds reads state.nodeId and sets engine.addContextId(ContextIdKeys.Node, state.nodeId) when node identity is enabled. |
tenant | Tenant 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. |
organization | Node 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). |
user | User 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). |
userOrganization | User 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,
AuthHeaderProcessoruses the JWTtidclaim 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
organizationin code; this corresponds to organisation scope in platform documentation. userOrganizationcarries the organisation DID from the authenticated user's own identity record and is distinct fromorganization, which identifies the whole tenant.
Context ID Handlers and Short Forms
Context IDs can be transformed by handlers before partitioning.
DidContextIdHandlervalidates DID format and returns the DID id segment as the short value (hex ids are compacted to base64url).TenantIdContextIdHandlervalidates 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.
| Key | Example Full Form | Example Short Form | How Short Form Is Produced |
|---|---|---|---|
node | did:iota:0x8c3f4a5e8d9a7b6c | jD9KXo2ae2xdTj8qGwydjn9qW0w9Lh8K | DidContextIdHandler takes the DID id segment and compacts hex to base64url. |
tenant | 7f3a9c1e0b4d6f8a2c5e7b9d1a3c4e6f | fzqcHgtNb4osXnudGjxObw | TenantIdContextIdHandler converts 32-hex tenant id to base64url. |
organization | did:iota:0x1234567890abcdef | Not produced | No default context-id handler is registered for this key. |
user | did:iota:0xabcdefabcdefabcd | Not produced | No default context-id handler is registered for this key. |
userOrganization | did:iota:0xdeadbeefdeadbee | Not produced | No 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:
- reads current context IDs using
ContextIdStore.getContextIds(), - derives a combined partition key via
ContextIdHelper.combinedContextKey(...), - uses that key to isolate stored data.
Examples:
- Entity storage connectors persist an internal
partitionIdand include it in get, set, remove, and query filtering. - Blob storage connectors compose keys as
<partitionKey>/<id>, with arootfallback where relevant.
Partition Key Derivation
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
partitionContextIdsis 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.