Adapters Overview
Adapters are the ingress layer of Ambiten's execution runtime.
They connect external frameworks and invocation environments to Ambiten's context-driven execution model by establishing a consistent execution boundary before application logic begins.
Adapters do not replace frameworks.
They normalize framework-specific execution into the same Ambiten runtime contract.
Whether execution begins in:
- Express,
- Fastify,
- NestJS,
- GraphQL,
- or AWS Lambda,
the underlying Ambiten execution model remains consistent once the operation enters the adapter boundary.
External Execution
↓
Framework Adapter
↓
Adapter Runtime
↓
AmbitenContext
↓
Application Logic
↓
AmbitenModel
↓
Runtime InfrastructureWhy Adapters Exist
Modern applications execute across very different environments.
An Express request follows middleware.
Fastify uses lifecycle hooks.
NestJS coordinates execution through modules and interceptors.
GraphQL is resolver-driven.
AWS Lambda uses invocation handlers.
Without a shared runtime boundary, each environment would need to independently solve concerns such as:
tenant resolution
request metadata
execution context
transaction propagation
async continuity
runtime correlationOver time, this creates duplicated infrastructure logic and inconsistent application behavior.
Ambiten adapters remove that fragmentation.
Each adapter translates its host environment into the same execution model.
Different frameworks
↓
different integration mechanisms
↓
same Ambiten runtime contractAdapters Do Not Initialize the Runtime
Runtime initialization and request execution are separate lifecycle concerns.
AmbitenBootstrapFactory prepares long-lived application infrastructure.
Adapters establish execution scope when work enters the application.
AmbitenBootstrapFactory
→ initializes runtime infrastructure
Framework Adapter
→ establishes execution boundary
AmbitenContext
→ carries execution state
MultiTenantManager
→ manages tenant infrastructure
AmbitenModel
→ performs data operationsThis distinction is important.
The adapter should be understood as an execution boundary initializer, not as the component that bootstraps the Ambiten runtime itself.
Adapter Responsibility
Adapters are intentionally narrow.
They do not perform business logic or persistence operations.
Their responsibility is to establish the correct Ambiten execution scope before downstream application code executes.
Depending on configuration, an adapter may:
- normalize an incoming request, operation, or invocation,
- resolve tenant identity,
- validate resolved tenancy,
- resolve request-scoped metadata,
- establish
AmbitenContext, - enter transaction-aware execution,
- preserve context through asynchronous execution,
- and then return control to the host framework lifecycle.
Conceptually:
Incoming Execution
↓
Normalize
↓
Resolve Context
↓
Validate Tenant
↓
AmbitenContext
↓
Optional Transaction Boundary
↓
Application ExecutionOnce the boundary exists, models, middleware, instrumentation, transactions, and tenant-aware database routing can operate against the same execution state.
The Shared Execution Model
Regardless of framework, execution follows the same conceptual path:
Framework / Invocation
↓
Adapter
↓
Adapter Runtime
↓
AmbitenContext
↓
Application Logic
↓
AmbitenModel
↓
MultiTenantManager
↓
MongoDBEach layer has a distinct responsibility.
Framework
→ transport and lifecycle
Adapter
→ execution boundary
Adapter Runtime
→ shared request/invocation orchestration
AmbitenContext
→ execution-scoped state
MultiTenantManager
→ tenant infrastructure
AmbitenModel
→ data operations
MongoDB
→ persistenceThis separation is what allows application logic to remain portable across supported environments.
Supported Adapters
Ambiten currently supports:
| Runtime | Integration Style |
|---|---|
| Express | Middleware |
| Fastify | Lifecycle hooks |
| NestJS | Module + global interceptor |
| GraphQL | Context factory |
| AWS Lambda | Wrapped handler |
The integration mechanism changes because each host environment has a different execution lifecycle.
The runtime contract does not.
Framework-Specific Entry Boundaries
Express
Express enters Ambiten through middleware.
Express Request
↓
Middleware
↓
Ambiten Adapter
↓
AmbitenContextFastify
Fastify enters through its request lifecycle.
Fastify Request
↓
Lifecycle Hook
↓
Ambiten Adapter
↓
AmbitenContextNestJS
NestJS enters through a framework-level interceptor.
NestJS Request
↓
Global Interceptor
↓
Ambiten Adapter
↓
AmbitenContextThe interceptor keeps the actual downstream Observable execution inside the active Ambiten context boundary.
GraphQL
GraphQL enters through context creation.
GraphQL Operation
↓
Context Factory
↓
Ambiten Adapter
↓
AmbitenContextAWS Lambda
Lambda enters through the wrapped handler.
Lambda Invocation
↓
Adapter Wrapper
↓
Ambiten Adapter
↓
AmbitenContextThe hosting mechanism changes.
The runtime behavior downstream remains consistent.
The Shared Adapter Runtime
Framework-specific adapters delegate common execution behavior to @ambiten/adapter-runtime.
Conceptually:
Express Adapter ───┐
Fastify Adapter ───┤
NestJS Adapter ────┤
GraphQL Adapter ───┤
Lambda Adapter ────┘
↓
@ambiten/adapter-runtime
↓
AmbitenContextThis shared layer centralizes behavior such as:
- tenant resolution,
- tenant validation,
- request ID resolution,
- database and collection context,
- debug metadata,
- logger metadata,
- custom execution metadata,
- transaction-aware execution,
- async context propagation.
Framework packages therefore remain focused on adapting framework lifecycle semantics rather than independently reimplementing Ambiten runtime behavior.
Tenant Resolution
Adapters operate at the request or invocation boundary, which makes them the natural place to determine tenant identity.
For example:
x-tenant-id: tenant5may become:
AmbitenContext.get().tenantId;
// "tenant5"The conceptual flow is:
Incoming Request
↓
TenantResolver
↓
tenant5
↓
AmbitenContextOnce resolved, downstream application code should normally use:
AmbitenContext.get().tenantIdrather than repeatedly inspecting the original transport-specific value.
Request Identity Is Not Tenant Infrastructure
Adapters identify the tenant associated with an execution.
They do not own tenant infrastructure.
TenantResolver
→ Who is this execution for?
AmbitenContext
→ Which tenant belongs to this execution?
MultiTenantManager
→ What resources belong to that tenant?
TenantConfigResolver
→ Where can an unknown tenant be found?For example, an adapter may resolve:
tenant5without knowing:
MongoDB URI
database name
connection state
deployment region
tenant metadataThose concerns remain under MultiTenantManager.
This separation prevents transport-specific code from becoming coupled to database infrastructure.
Tenant Validation
Adapters can validate resolved tenant identity before application execution begins.
Conceptually:
Request
↓
Resolve tenant
↓
tenant5
↓
Validate
↓
Valid?
┌──┴──┐
yes no
↓ ↓
Context rejectValidation may be asynchronous.
For example:
{
tenancy: {
header: "x-tenant-id",
validate: async (tenantId) => {
const tenant =
await MultiTenantManager
.resolveTenant(tenantId);
if (!tenant) {
throw new Error(
`Tenant with ID "${tenantId}" not found.`
);
}
return true;
}
}
}This allows validation to work with both statically registered and dynamically discovered tenants.
Dynamic Tenants
Adapters do not need to know whether a tenant existed when the application started.
Suppose an adapter resolves:
tenant5but tenant5 is not yet registered.
A model operation can trigger:
AmbitenContext
tenantId = tenant5
↓
MultiTenantManager
↓
tenant5 registered?
↓ no
TenantConfigResolver
↓
external lookup
↓
register tenant5
↓
getClient()
↓
tenant databaseFrom the adapter's perspective:
static tenantand:
dynamic tenantare the same identity problem.
Tenant discovery belongs to the runtime infrastructure layer.
Custom Tenant Resolution
Adapters are not restricted to a fixed tenant header.
Custom resolvers can derive tenant identity from framework or application-specific information.
Conceptually:
{
resolvers: {
tenantId: async (req) => {
return resolveTenantForRequest(
req
);
}
}
}Tenant identity may originate from:
- headers,
- authenticated identity,
- token claims,
- cookies,
- subdomains,
- route information,
- gateway metadata,
- invocation metadata,
- application-defined request state.
The downstream runtime only needs the resolved identity:
External Input
↓
TenantResolver
↓
tenantId
↓
AmbitenContextRequest Metadata
The shared adapter runtime can carry more than tenant identity.
Execution context may include:
tenantId
requestId
dbName
collectionName
debug state
logger metadata
custom metadata
transaction sessionFor example:
AmbitenContext.get();may expose:
{
tenantId: "tenant5",
requestId: "req_123",
dbName: undefined,
collectionName: undefined
}These values describe the current execution.
They should not be confused with long-lived tenant configuration or process infrastructure.
Execution Scope
Every adapter establishes a scope appropriate to its host environment.
Express
→ request scope
Fastify
→ request scope
NestJS
→ request scope
GraphQL
→ operation scope
Lambda
→ invocation scopeEach execution receives its own AmbitenContext.
Execution A
→ Context A
Execution B
→ Context B
Execution C
→ Context CLong-lived infrastructure may be shared.
Execution state must remain isolated.
Async Context Propagation
Adapters preserve Ambiten's execution context across asynchronous work associated with the active scope.
For example:
const before =
AmbitenContext
.get()
.tenantId;
await someAsyncOperation();
const after =
AmbitenContext
.get()
.tenantId;For the same execution:
before === after;This allows tenant identity and other runtime metadata to remain available through:
controller
↓
service
↓
await
↓
repository
↓
modelwithout manually passing execution state through every function.
Transaction-Aware Execution
Adapters can optionally establish transaction-aware execution boundaries.
Conceptually:
Request / Invocation
↓
Adapter
↓
AmbitenContext
↓
Transaction Boundary
↓
Application LogicFor adapters supporting:
enableTransactions: truethe entire execution can run inside Ambiten's transaction-aware boundary.
Applications may also use explicit:
AmbitenContext.withTransaction(...)when only a particular operation needs atomic consistency.
The distinction is:
Adapter transaction option
→ execution-wide transaction
withTransaction(...)
→ explicit operation transactionTransaction policy should reflect consistency requirements rather than being enabled indiscriminately.
Runtime Portability
One of the main goals of the adapter system is portability.
The same model operation:
await UserModel.find({});should participate in the same Ambiten runtime model whether it executes inside:
Express middleware
Fastify lifecycle
NestJS controller/service execution
GraphQL resolver execution
Lambda handler executionThe framework determines how execution reaches Ambiten.
It does not change how Ambiten models behave once execution is inside the runtime.
Application Services Remain Framework-Independent
Adapter-managed context means domain services do not need framework request objects merely to determine execution identity.
Without this boundary, applications may evolve toward:
Framework Request
↓
Controller
↓ passes request
Service
↓ passes request
RepositoryWith Ambiten:
Framework
↓
Adapter
↓
AmbitenContext
↓
Controller / Resolver / Handler
↓
Service
↓
RepositoryA service can access:
const {
tenantId,
requestId
} = AmbitenContext.get();without depending directly on Express, Fastify, NestJS, GraphQL, or AWS Lambda APIs.
Adapters and AmbitenBootstrapFactory
Adapters and bootstrap solve different lifecycle problems.
BOOTSTRAP
─────────
AmbitenBootstrapFactory
↓
runtime infrastructure ready
EXECUTION
─────────
Framework / Invocation
↓
Adapter
↓
AmbitenContextThe factory runs during application startup.
Adapters operate when requests, operations, or invocations arrive.
This separation keeps startup orchestration out of application request paths.
Adapters and MultiTenantManager
Adapters carry tenant identity.
MultiTenantManager owns tenant infrastructure.
Adapter
→ tenantId
MultiTenantManager
→ tenant configuration
→ dynamic resolution
→ client lifecycle
→ runtime tenant stateThis allows the same adapter configuration to work whether tenants are:
- registered during startup,
- registered programmatically,
- dynamically discovered,
- lazily connected.
The adapter does not need to know which strategy is being used.
Authentication and Authorization
Tenant resolution is not authentication or authorization.
A request containing:
x-tenant-id: tenant5may correctly identify a tenant without proving the caller has permission to access it.
A secure application may use:
Request
↓
Authentication
↓
Authenticated Identity
↓
Tenant Resolution
↓
Authorization
↓
AmbitenContext
↓
ApplicationThe exact ordering may depend on the host framework and resolver design.
Ambiten provides runtime identity and propagation.
Application security policy remains an application responsibility.
Infrastructure Reuse vs Execution Isolation
Adapters establish short-lived execution context around application work.
Runtime infrastructure may be much longer lived.
LONG-LIVED
──────────
AmbitenRuntime
MultiTenantManager
MongoDB clients
Redis clients
logging infrastructure
EXECUTION-SCOPED
────────────────
AmbitenContext
tenantId
requestId
transaction session
operation metadataThis distinction is especially visible in AWS Lambda, where warm execution environments may reuse runtime infrastructure while every invocation still receives a fresh execution context.
The same principle applies conceptually to long-running HTTP servers.
Package Boundaries
Context-aware execution depends on a compatible package graph.
For ESM applications:
ESM Application
↓
ESM Framework Adapter
↓
ESM adapter-runtime
↓
ESM CoreFor CommonJS applications:
CommonJS Application
↓
CommonJS Framework Adapter
↓
CommonJS adapter-runtime
↓
CommonJS CoreApplications should import Ambiten packages through public package entry points rather than internal build paths.
For example:
import {
AmbitenContext
} from "@ambiten/core";and the public entry point for the selected adapter.
Avoid:
@ambiten/core/dist/...
@ambiten/adapter-runtime/dist/...Public package exports select the appropriate module format.
Why Package-Boundary Testing Matters
Adapters operate across npm package boundaries, not only source-code boundaries.
Production-like integration testing should therefore verify built packages from an external consumer environment.
Conceptually:
pack Core
↓
pack adapter-runtime
↓
pack framework adapter
↓
install into isolated consumer
↓
run real executionAmbiten's adapter/runtime boundary should preserve context through both:
ESM consumerand:
CommonJS consumerThis protects runtime behavior that cannot be fully validated through monorepo unit tests alone.
Choosing an Adapter
Adapter choice should follow the host execution environment.
Express
Use Express when the application already uses middleware-oriented HTTP architecture.
Fastify
Use Fastify for hook-driven services and applications built around Fastify's lifecycle and plugin system.
NestJS
Use NestJS when the application relies on modules, dependency injection, controllers, providers, and framework-managed execution pipelines.
GraphQL
Use GraphQL integration when execution is resolver-driven and context creation is the natural operation boundary.
AWS Lambda
Use Lambda integration for serverless invocation-driven workloads.
The adapter changes how execution enters Ambiten.
It does not change the Ambiten runtime model after execution begins.
Architectural Boundary
Adapters define the separation between transport and runtime execution.
Framework / Platform
→ transport and lifecycle
Adapter
→ runtime ingress
Adapter Runtime
→ shared execution setup
AmbitenContext
→ execution state
MultiTenantManager
→ tenant infrastructure
AmbitenModel
→ data operations
MongoDB
→ persistenceWithout this separation, transport-specific concerns tend to spread into services, models, and infrastructure code.
Adapters preserve that boundary.
Recommended Mental Model
A useful way to understand the complete adapter system is:
STARTUP
│
AmbitenBootstrapFactory
│
Runtime Ready
│
▼
EXECUTION
│
Express / Fastify / NestJS
GraphQL / AWS Lambda
│
▼
Adapter
│
▼
Adapter Runtime
│
▼
Tenant Resolution
│
▼
AmbitenContext
│
▼
Application Logic
│
▼
AmbitenModel
│
▼
MultiTenantManager
│
▼
Tenant DatabaseThe framework determines the entry mechanism.
Ambiten determines the execution model.
Summary
Adapters are the ingress boundary into Ambiten's execution runtime.
They normalize framework-specific requests, operations, and invocations into one shared context-aware model.
Adapters:
- establish execution scope,
- resolve tenant identity,
- validate tenancy when configured,
- propagate request metadata,
- preserve context through asynchronous execution,
- optionally establish transaction-aware boundaries,
- keep framework-specific concerns outside Core,
- allow dynamic tenant infrastructure to be resolved downstream.
The shared architecture is:
Framework / Invocation
↓
Adapter
↓
Adapter Runtime
↓
AmbitenContext
↓
Application
↓
AmbitenModel
↓
MultiTenantManager
↓
Tenant DatabaseThis is what allows Ambiten applications to remain portable, predictable, tenant-aware, and operationally consistent across multiple execution environments.
