GraphQL Framework Track
Ambiten integrates with GraphQL at the execution boundary.
The GraphQL adapter supports both:
- Apollo Server
- GraphQL Yoga
Both integrations preserve the same Ambiten execution model:
GraphQL Request
↓
GraphQL Server
↓
Ambiten GraphQL Adapter
↓
Adapter Runtime
↓
AmbitenContext
↓
Resolvers
↓
Application Services
↓
AmbitenModel
↓
MongoDBThe GraphQL server may change.
The application execution model does not.
Installation
Install Ambiten and the GraphQL adapter:
npm install \
@ambiten/core \
@ambiten/adapter-graphqlThen install the GraphQL server used by your application.
Apollo Server
npm install \
@apollo/server \
graphqlGraphQL Yoga
npm install \
graphql-yoga \
graphql@ambiten/adapter-graphql does not require applications to use a particular GraphQL server internally.
The adapter exposes framework-facing integration points while the shared Ambiten adapter runtime owns execution context.
Package version
This framework track targets:
@ambiten/adapter-graphql@2.xVersion 2 introduces execution-scoped integrations for Apollo Server and GraphQL Yoga.
The primary APIs are:
createApolloAdapter()
createYogaAdapter()Earlier context-factory APIs remain available for compatibility, but they should not be used as the primary execution integration.
Shared application layer
The Apollo and Yoga examples use the same GraphQL schema, resolvers, and Ambiten models.
A useful project structure is:
examples/
└── ambiten-tutorials/
└── framework-tracks/
└── graphql/
├── src/
│ ├── shared/
│ │ ├── type-defs.ts
│ │ ├── resolvers.ts
│ │ ├── runtime.ts
│ │ └── user.model.ts
│ │
│ ├── apollo.ts
│ └── yoga.ts
│
├── package.json
├── tsconfig.json
└── .env.exampleThe important separation is:
shared/
→ GraphQL schema
→ resolvers
→ models
→ application logic
apollo.ts
→ Apollo ingress
yoga.ts
→ Yoga ingressThis allows the same application layer to run under either GraphQL server.
Shared schema
// src/shared/type-defs.ts
export const typeDefs = `#graphql
type User {
_id: ID!
name: String!
email: String!
}
type RuntimeContext {
tenantId: String
requestId: String
}
input CreateUserInput {
name: String!
email: String!
}
type Query {
users: [User!]!
runtime: RuntimeContext!
}
type Mutation {
createUser(
input: CreateUserInput!
): User!
}
`;The runtime field is included only to make the execution boundary visible during development.
Shared resolvers
// src/shared/resolvers.ts
import {
AmbitenContext
} from "@ambiten/core";
import {
UserModel
} from "./user.model";
export const resolvers = {
Query: {
users: async () => {
return UserModel.find({});
},
runtime: () => {
const ctx =
AmbitenContext.get();
return {
tenantId:
ctx.tenantId,
requestId:
ctx.requestId
};
}
},
Mutation: {
createUser: async (
_parent: unknown,
args: {
input: {
name: string;
email: string;
};
}
) => {
return UserModel.create(
args.input
);
}
}
};Notice what the resolvers do not receive manually:
tenantId
dbName
ClientSession
requestIdThe GraphQL adapter establishes the Ambiten execution boundary before resolver execution begins.
Runtime-aware model operations can therefore consume the active execution context without manually threading infrastructure state through resolver arguments.
Apollo Server
Apollo Server integrates through:
createApolloAdapter()The adapter wraps Apollo-compatible GraphQL HTTP execution so the Ambiten runtime remains active while resolvers execute.
Apollo setup
// src/apollo.ts
import {
ApolloServer
} from "@apollo/server";
import {
startStandaloneServer
} from "@apollo/server/standalone";
import {
createApolloAdapter
} from "@ambiten/adapter-graphql";
import {
MultiTenantManager
} from "@ambiten/core";
import {
typeDefs
} from "./shared/type-defs";
import {
resolvers
} from "./shared/resolvers";
import {
initializeRuntime
} from "./shared/runtime";
await initializeRuntime();
const server =
new ApolloServer({
typeDefs,
resolvers
});
const adapter =
createApolloAdapter();
adapter.install(
server,
{
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;
}
}
}
);
const { url } =
await startStandaloneServer(
server,
{
listen: {
port: 4000
}
}
);
console.log(
`Apollo ready at ${url}`
);Application code only interacts with:
createApolloAdapter()The adapter internally coordinates the GraphQL request with Ambiten's shared adapter runtime.
Application code does not need to use:
runWithAdapterContext
AmbitenRequestLike
AsyncLocalStorage
adapter-runtime internalsApollo runtime flow
HTTP Request
↓
Apollo Server
↓
createApolloAdapter()
↓
GraphQL execution boundary
↓
Adapter Runtime
↓
Tenant Resolution
↓
AmbitenContext
↓
Resolvers
↓
Services
↓
AmbitenModel
↓
MongoDB
↓
GraphQL ResponseThe important boundary is the GraphQL operation itself.
Ambiten does not establish context only while the Apollo context object is being constructed.
The runtime remains active while application execution proceeds.
GraphQL Yoga
GraphQL Yoga integrates through:
createYogaAdapter()The Yoga adapter participates in Yoga's execution pipeline so queries, mutations, subscriptions, and streaming execution can enter the Ambiten runtime boundary.
Yoga setup
// src/yoga.ts
import {
createServer
} from "node:http";
import {
createSchema,
createYoga
} from "graphql-yoga";
import {
createYogaAdapter
} from "@ambiten/adapter-graphql";
import {
MultiTenantManager
} from "@ambiten/core";
import {
typeDefs
} from "./shared/type-defs";
import {
resolvers
} from "./shared/resolvers";
import {
initializeRuntime
} from "./shared/runtime";
await initializeRuntime();
const ambiten =
createYogaAdapter({
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;
}
}
});
const yoga =
createYoga({
schema: createSchema({typeDefs,resolvers}),
plugins: [
ambiten
]
});
const server = createServer(yoga);
server.listen(
4001,
() => {
console.log(
"Yoga ready at http://localhost:4001/graphql"
);
}
);The application resolver layer remains unchanged.
Yoga runtime flow
HTTP Request
↓
GraphQL Yoga
↓
createYogaAdapter()
↓
Execute / Subscribe
↓
Adapter Runtime
↓
Tenant Resolution
↓
AmbitenContext
↓
Resolvers
↓
Services
↓
AmbitenModel
↓
MongoDBFor streaming or subscription execution, Ambiten preserves the already-resolved execution identity when the iterator continues.
The original ingress tenant resolution is not treated as a new request for every emitted result.
Run the examples
Apollo:
npm run dev:apolloYoga:
npm run dev:yogaA project may expose scripts such as:
{
"scripts": {
"dev:apollo": "tsx watch src/apollo.ts",
"dev:yoga": "tsx watch src/yoga.ts"
}
}Query runtime state
Run the same query against either server:
query {
runtime {
tenantId
requestId
}
users {
_id
name
email
}
}Send:
x-tenant-id: tenant-aThe result demonstrates that resolver execution is running inside the resolved Ambiten context.
Create a user
mutation {
createUser(
input: {
name: "GraphQL User"
email: "graphql@example.com"
}
) {
_id
name
email
}
}The resolver remains:
return UserModel.create(
args.input
);The tenant identity does not need to be forwarded manually.
Application context still works
Ambiten does not replace normal GraphQL application context.
For example, Apollo may still define application-specific context:
await startStandaloneServer(
server,
{
context:
async ({ req }) => {
return {
user:
await authenticate(
req.headers
.authorization
)
};
}
}
);The Apollo adapter preserves that context while adding Ambiten execution information.
Conceptually:
GraphQL context
→ authenticated user
→ DataLoaders
→ API clients
→ application services
AmbitenContext
→ tenantId
→ requestId
→ runtime metadata
→ execution infrastructure
→ transaction session when explicitly activeThese concerns are related but not identical.
Tenant resolution is not authorization
The GraphQL adapter may resolve tenant identity from:
x-tenant-idbut tenant resolution does not prove that the caller is authorized to act for that tenant.
A complete application boundary remains:
Authentication
↓
Who is the caller?
Authorization
↓
May this caller act for this tenant?
Tenant Resolution
↓
Which tenant execution is this for?
AmbitenContext
↓
Runtime executionApplications should perform authentication and authorization according to their security architecture.
Ambiten then carries the validated execution identity through infrastructure-aware application work.
Server-owned infrastructure
Tenant identity may originate at ingress.
Database and collection routing should normally remain server-owned.
Recommended:
Client
→ x-tenant-id
Server
→ tenant configuration
→ database resolution
→ collection/model configurationAvoid treating client-supplied database or collection names as an authorization mechanism.
Tenant identity and tenant infrastructure remain separate concepts.
Transactions
@ambiten/adapter-graphql@2.x does not enable automatic GraphQL-operation-wide transactions through:
enableTransactions: trueGraphQL can complete an execution while returning resolver failures in its result.
That means a generic operation-wide transaction boundary cannot safely infer:
GraphQL result returned
=
transaction should commitFor GraphQL mutations, establish transactions explicitly around the workflow that requires atomicity.
Conceptually:
Mutation Resolver
↓
Application Service
↓
Explicit Transaction Boundary
↓
Model A
↓
Model B
↓
Commit / RollbackThis keeps transaction ownership aligned with the actual business operation rather than the GraphQL transport.
Context factories
Earlier versions exposed integrations centered around:
createApolloContextFactory()
createYogaContextFactory()These APIs may remain available for compatibility, but they should not be used as the primary Ambiten integration.
Why?
Creating a GraphQL context object and executing GraphQL resolvers are different lifecycle stages.
A context factory can produce:
{
tenantId: "tenant-a"
}without guaranteeing that the active Ambiten runtime remains alive later when the resolver executes.
Version 2 solves this by integrating with actual GraphQL execution.
Prefer:
createApolloAdapter()and:
createYogaAdapter()for new applications.
Framework-neutral adapter design
@ambiten/adapter-graphql intentionally avoids owning Apollo, Yoga, or GraphQL framework type hierarchies.
Internally the package uses small structural execution contracts and converts ingress data into Ambiten's framework-neutral request representation.
Conceptually:
Apollo ─────┐
│
Yoga ───────┤
↓
GraphQL request normalization
↓
AmbitenRequestLike
↓
Adapter Runtime
↓
AmbitenContextThis keeps Ambiten's execution architecture independent from framework release cycles while allowing applications to choose the GraphQL server appropriate for their environment.
Apollo and Yoga share the same application
The two integrations differ only at ingress.
GraphQL Operation
↓
┌─────────┴─────────┐
│ │
Apollo Server GraphQL Yoga
│ │
↓ ↓
createApolloAdapter createYogaAdapter
│ │
└─────────┬─────────┘
↓
Adapter Runtime
↓
AmbitenContext
↓
Resolvers
↓
Services
↓
AmbitenModel
↓
MongoDBThat is the main lesson of this framework track.
Apollo and Yoga provide different GraphQL server environments.
Ambiten provides the same execution model underneath both.
What just happened
The GraphQL server received an operation.
The Ambiten GraphQL adapter converted that framework-specific execution into an Ambiten execution boundary.
Tenant identity and execution metadata were resolved once at ingress.
AmbitenContext remained active while resolvers, nested asynchronous calls, services, and models executed.
The model layer then resolved the effective model context and tenant-aware infrastructure before MongoDB performed the operation.
At no point did application resolvers need to manually propagate the tenant through every function call.
Runtime flow
GraphQL Request
↓
Apollo Server / GraphQL Yoga
↓
Ambiten GraphQL Adapter
↓
Adapter Runtime
↓
Tenant Resolution
↓
AmbitenContext
↓
Resolver
↓
Application Service
↓
AmbitenModel
↓
Effective ModelContext
↓
Tenant Infrastructure Resolution
↓
MongoDB
↓
GraphQL ResultCheckpoint
At this point you have used Ambiten with:
Apollo Server ✓
GraphQL Yoga ✓
Execution-scoped context ✓
Tenant resolution ✓
Tenant validation ✓
Async resolver propagation ✓
Shared resolver layer ✓
Shared model layer ✓
Concurrent execution isolation ✓
Streaming continuation support ✓
Explicit transaction ownership ✓The GraphQL server controls GraphQL execution.
Ambiten controls runtime consistency inside that execution boundary.
Return to Framework Tracks, compare the NestJS boundary, or revisit Transaction Continuity
