Schema
AmbitenSchema defines the structural and behavioral contract used by AmbitenModel.
A schema describes more than document shape.
It can define:
document structure
validation behavior
middleware
normalization
lifecycle policy
persistence-oriented ruleswhile remaining independent from request routing, tenant discovery, database connections, and application workflow orchestration.
In the Ambiten model runtime, the schema participates in an operation after AmbitenModel has resolved the effective ModelContext.
Conceptually:
AmbitenContext
↓
AmbitenModel.mergeCtx()
↓
Effective ModelContext
↓
AmbitenSchema / Middleware
↓
Model Operation
↓
Infrastructure Resolution
↓
MongoDBSchema defines persistence behavior. Model binds that behavior to execution.
What AmbitenSchema Represents
An AmbitenSchema is the persistence contract associated with a model's document structure and operation behavior.
For example:
import {
AmbitenSchema
} from "@ambiten/core";
const userSchema =
new AmbitenSchema({
name: "string",
email: "string",
createdAt: "date"
});The schema definition is static.
It does not change simply because:
the request changes
the tenant changes
a transaction begins
a worker executes the model
the application runs behind another frameworkThose are execution concerns.
The schema remains a reusable definition.
Structural and Behavioral Responsibilities
A schema can participate in two broad categories of model behavior.
Structural Behavior
document shape
field definitions
validation
type relationships
data integrityRuntime Persistence Behavior
middleware
normalization
soft-delete policy
lifecycle behavior
audit enrichment
operation-oriented policyThese concerns stay close to the persistence boundary rather than being distributed across controllers and services.
Schema Does Not Own Execution Context
AmbitenSchema does not establish the current request or operation context.
The execution boundary is established before the schema participates.
For normal model execution:
Framework Adapter
or
AmbitenContext.run(...)
↓
AmbitenContext
↓
AmbitenModel
↓
mergeCtx(...)
↓
Effective ModelContext
↓
Schema / MiddlewareThe schema consumes operation state supplied through the model runtime.
It does not independently determine:
which request is active
which tenant entered the system
which MongoClient should be used
which database connection should be createdThose responsibilities belong to other runtime layers.
ModelContext and Schema Execution
ModelContext is the operation-facing context contract shared across the model runtime.
export type ModelContext = {
tenantId?: string;
requestId?: string;
dbName?: string;
db?: Db;
collectionName?: string;
config?: AmbitenConfig;
session?: ClientSession;
withDeleted?: boolean;
onlyDeleted?: boolean;
hardDelete?: boolean;
};It contains execution values relevant to model, schema, and persistence behavior.
The full runtime state lives in AmbitenContextState.
The model derives the operation-facing context from that execution state.
Conceptually:
AmbitenContextState
↓
AmbitenContext.get()
+
explicit ModelContext
+
model defaults
↓
AmbitenModel.mergeCtx()
↓
Effective ModelContextThat effective context can then participate throughout the operation.
Effective ModelContext
├── Schema
├── Middleware
├── Model Operation
└── DbProviderThis gives the model runtime one consistent operation context rather than forcing every layer to reconstruct runtime state independently.
Structural Typing
AmbitenSchema supports TypeScript generics.
Use the Document contract exported by @ambiten/core.
import type {
Document
} from "@ambiten/core";
interface User
extends Document {
name: string;
email: string;
}The document contract can then be used with the schema:
const userSchema =
new AmbitenSchema<User>({
name: "string",
email: "string"
});and with its corresponding model:
const UserModel =
new AmbitenModel<User>({
collectionName: "users",
schema: userSchema,
provider: client
});Using the same document type across both layers keeps the schema and model generic contracts compatible.
NOTE
Use the Document type exported by @ambiten/core for document types passed to AmbitenSchema<T> and AmbitenModel<T>.
import type {
Document
} from "@ambiten/core";The same document type should be used by the corresponding schema and model.
Do not substitute an unrelated MongoDB Document type when the Ambiten APIs expect Ambiten's exported document contract.
Validation
Schemas form a validation boundary around model persistence behavior.
For example:
await UserModel.create({
name: "John",
email: "john@example.com"
});The schema can validate the document before persistence reaches MongoDB.
This allows persistence-oriented validation rules to remain centralized.
Instead of duplicating structural validation across:
controller
service
repository
background worker
GraphQL resolverthe schema can keep those rules close to the model's persistence boundary.
Schema and Model Typing
A useful relationship is:
Document Type
↓
AmbitenSchema<T>
↓
AmbitenModel<T>For example:
interface User
extends Document {
name: string;
email: string;
}
const userSchema =
new AmbitenSchema<User>({
name: "string",
email: "string"
});
const UserModel =
new AmbitenModel<User>({
collectionName: "users",
schema: userSchema,
provider: client
});This gives the model a consistent structural contract from schema definition through persistence operations.
Middleware Registration
Schemas are a primary registration surface for model middleware.
For example:
userSchema.pre(
"create",
async (ctx) => {
if (ctx.doc) {
ctx.doc.createdAt =
new Date();
}
}
);Middleware can participate in concerns such as:
validation
normalization
auditing
soft-delete behavior
policy checks
logging
instrumentation
operation shaping
result processingThe middleware is associated with schema/model execution rather than the host framework.
Runtime-Aware Middleware
Middleware executes as part of the current model operation.
Conceptually:
Effective ModelContext
↓
before middleware
↓
model operation
↓
after middlewareThis means middleware can participate in runtime-aware behavior using the operation context made available by the model execution pipeline.
For example:
userSchema.post(
"create",
async (ctx) => {
console.log(
`Created in tenant ${ctx.tenantId}`
);
}
);Here, tenantId belongs to the effective model operation context.
The schema did not resolve the tenant itself.
The path is:
TenantResolver / Explicit Context
↓
AmbitenContext
↓
AmbitenModel.mergeCtx()
↓
ModelContext.tenantId
↓
Schema MiddlewareThat distinction keeps tenant identification outside schema behavior.
Middleware and Transactions
The same principle applies to transactions.
When a transaction session is active:
Transaction Boundary
↓
AmbitenContext.session
↓
AmbitenModel.mergeCtx()
↓
ModelContext.session
↓
Schema / Middleware / OperationSchema middleware can therefore execute as part of the same model operation that participates in the transaction.
The schema does not create or commit the transaction.
The surrounding transaction boundary owns:
session lifecycle
commit
rollbackThe schema participates inside that boundary.
Runtime Participation
The detailed flow is:
Execution Boundary
↓
AmbitenContext
↓
AmbitenModel.mergeCtx()
↓
Effective ModelContext
↓
Schema Validation
↓
Middleware
↓
Model Operation
↓
DbProvider / AmbitenClient
↓
MongoDBThe schema participates in model execution before, around, and where configured after persistence behavior.
It does not replace the model or infrastructure layers.
Schema vs Model Responsibilities
AmbitenSchema and AmbitenModel are closely related but intentionally separate.
AmbitenSchema
→ document structure
→ validation
→ middleware
→ persistence policy
→ lifecycle configuration
AmbitenModel
→ collection boundary
→ operation execution
→ effective context resolution
→ middleware orchestration
→ infrastructure coordinationThe model binds schema behavior to the current operation.
The schema defines what behavior should apply.
Collection Ownership
The schema does not own the MongoDB collection.
Collection ownership remains with AmbitenModel.
For example:
const UserModel =
new AmbitenModel({
collectionName: "users",
schema: userSchema,
provider: client
});Conceptually:
AmbitenSchema
→ document and behavior contract
AmbitenModel
→ collection boundaryA supported ModelContext.collectionName override may influence the effective collection during execution, but that resolution remains part of the model operation rather than schema definition.
Provider Independence
A schema does not resolve database infrastructure.
It does not need to know whether the model uses:
a fixed AmbitenClient
a database-scoped provider
a tenant-scoped provider
MultiTenantManager
dynamic tenant infrastructureThe broader model path is:
AmbitenSchema
↓
AmbitenModel Operation
↓
Effective ModelContext
↓
DbProvider
↓
AmbitenClient
↓
MongoDBThis allows the same schema definition to remain usable across different infrastructure topologies.
Tenant Independence
A schema should not be defined per request tenant merely because runtime infrastructure changes.
For example:
tenant-a
↓
UserModel
↓
userSchema
tenant-b
↓
UserModel
↓
userSchemaThe same schema can participate in both executions.
The difference belongs to:
AmbitenContext
ModelContext
provider resolution
tenant infrastructurenot to the structural schema definition.
Soft-Delete Operation Context
ModelContext also carries operation-oriented persistence controls such as:
withDeleted
onlyDeleted
hardDeleteThese values belong to the effective operation context rather than global schema mutation.
Conceptually:
Model Operation
↓
Effective ModelContext
├── withDeleted
├── onlyDeleted
└── hardDelete
↓
Schema / Model PolicyThis allows operation-specific behavior to vary without redefining the schema.
Lifecycle Policies
Schemas can define persistence-oriented lifecycle configuration.
For example:
schema.setGCConfig({
ttlField: "createdAt",
expiresIn: "30d",
softDelete: true
});This places lifecycle policy close to the data contract.
The important distinction is:
Schema
→ declares lifecycle policy
Managed runtime / lifecycle services
→ execute or orchestrate that policyThe schema does not itself become the process-level garbage-collection runner.
This keeps definition and execution responsibilities separate.
Garbage Collection Relationship
Conceptually:
AmbitenSchema
↓
GC / TTL configuration
↓
Runtime lifecycle infrastructure
↓
cleanup behaviorThis is consistent with Ambiten's wider architecture:
definition
→ stable
runtime execution
→ dynamicThe schema declares what the persistence lifecycle should mean.
The runtime manages when and how lifecycle services execute.
Static Definition, Dynamic Execution
A schema is defined once:
const userSchema =
new AmbitenSchema({
name: "string",
email: "string"
});But it can participate in many independent executions.
Schema Definition
│
├── Request A
│ tenant-a
│
├── Request B
│ tenant-b
│
├── Worker C
│ transaction
│
└── Job D
explicit databaseThe schema does not need to be recreated for each execution.
Static definition. Dynamic execution.
Concurrent Execution
One schema and model definition may be used concurrently by multiple operations.
Execution A
tenantId = tenant-a
↓
Effective ModelContext A
↓
userSchema
Execution B
tenantId = tenant-b
↓
Effective ModelContext B
↓
userSchemaThe shared schema definition remains static.
Execution-specific state remains in the operation context.
This is why request or tenant state should not be stored as mutable state directly on shared schema objects.
Framework Independence
A schema should not depend on framework-specific request types.
Avoid coupling schema behavior directly to:
Express Request
Fastify Request
NestJS ExecutionContext
GraphQL resolver context
Lambda eventInstead:
Framework
↓
Execution Boundary
↓
AmbitenContext
↓
ModelContext
↓
Schema / MiddlewareThis allows the same schema to participate behind different adapters and execution environments.
Recommended Design Approach
Schemas should remain focused on persistence-oriented structure and behavior.
Good schema concerns include:
field definitions
validation
normalization
middleware
soft-delete policy
lifecycle configuration
audit enrichment
persistence-oriented policyConcerns that should generally remain outside schemas include:
HTTP routing
controller behavior
application workflow orchestration
authentication flow
tenant discovery
MongoClient creation
database connection lifecycle
external service orchestrationKeeping those boundaries explicit prevents the schema layer from becoming an application service layer.
Middleware Design
Schema middleware should generally remain:
small
focused
composable
deterministic
operation-orientedMiddleware is especially useful for behavior that should consistently surround persistence operations regardless of where the operation originated.
For example:
Express
Fastify
GraphQL
NestJS
Lambda
Worker
↓
same model
↓
same schema middlewareThe host environment changes.
The persistence policy does not.
Relationship with AmbitenModel
The relationship can be summarized as:
AmbitenSchema
→ defines structure and persistence behavior
AmbitenModel
→ executes operations using that definitionAt runtime:
AmbitenContext
↓
AmbitenModel.mergeCtx()
↓
Effective ModelContext
↓
AmbitenSchema behavior
↓
Model operationThe schema defines policy.
The model binds that policy to an execution.
Relationship with AmbitenClient
The schema does not talk directly to MongoDB infrastructure as its primary responsibility.
The normal model path remains:
AmbitenSchema
↓
AmbitenModel
↓
DbProvider
↓
AmbitenClient
↓
MongoDBAmbitenClient provides database, client, and session capability.
AmbitenSchema defines structural and behavioral persistence rules.
The model connects those layers during an operation.
What AmbitenSchema Does Not Own
AmbitenSchema does not own:
request ingress
execution-boundary creation
AmbitenContext lifecycle
tenant identification
tenant authorization
dynamic tenant discovery
MongoClient lifecycle
database routing
collection ownership
transaction commit / rollback
application workflowsThose responsibilities belong to other runtime layers.
Mental Model
The complete mental model is:
AmbitenContext
= execution state
AmbitenModel
= operation coordinator and context binder
ModelContext
= effective model-operation state
AmbitenSchema
= document + persistence behavior contract
DbProvider
= database/client/session contract
AmbitenClient
= MongoDB infrastructure capability
MongoDB
= persistenceOr more compactly:
Schema defines structure and persistence policy.
Model binds that policy to execution.
ModelContext carries operation state.
Client provides infrastructure.
MongoDB performs persistence.Design Principles
Validation, normalization, lifecycle configuration, middleware, and persistence-oriented policies remain close to the schema instead of being duplicated across application services.
Schema and middleware behavior participate in the effective ModelContext resolved by AmbitenModel, allowing tenant identity, transaction sessions, operation controls, and other execution state to influence persistence behavior.
Cross-cutting persistence concerns can be defined once at the schema boundary and applied consistently to model operations that use that schema.
Schema definitions remain stable while execution-specific state changes per request, tenant, transaction, or runtime boundary, preserving predictable behavior for equivalent operation contexts.
These principles allow schema definitions to remain stable while their behavior participates in request-aware, tenant-aware, transaction-aware, and middleware-driven model execution.
Summary
AmbitenSchema defines the structural and behavioral persistence contract used by AmbitenModel.
It can define:
- document structure,
- TypeScript relationships,
- validation,
- middleware,
- normalization,
- soft-delete policy,
- lifecycle configuration,
- persistence-oriented runtime behavior.
It participates in execution through the effective ModelContext resolved by AmbitenModel.
The runtime path is:
AmbitenContext
↓
AmbitenModel.mergeCtx()
↓
Effective ModelContext
↓
AmbitenSchema / Middleware
↓
Model Operation
↓
DbProvider
↓
AmbitenClient
↓
MongoDBThe architectural distinction is:
AmbitenContextcarries execution state.AmbitenModelbinds that state to an operation.AmbitenSchemadefines the persistence behavior that participates in that operation.
The schema remains static.
The execution around it changes dynamically.
Static definition. Dynamic execution.
