MultiTenantManager
MultiTenantManager is Ambiten's runtime registry and resource manager for multi-tenant applications.
It is responsible for knowing which tenants are currently registered, resolving tenants that are not yet known locally, managing their MongoDB client lifecycle, and exposing runtime tenancy state.
It does not determine which tenant an incoming request belongs to.
That responsibility belongs to tenant resolution and AmbitenContext.
Request
↓
TenantResolver
↓
AmbitenContext.tenantId
↓
MultiTenantManager
├── registered tenant?
│ ↓ yes
│ use config
│
└── no
↓
TenantConfigResolver
↓
resolve configuration
↓
register tenant
↓
getClient()
↓
tenant databaseAt a high level:
AmbitenContext
→ Which tenant belongs to this execution?
MultiTenantManager
→ What configuration and resources belong to that tenant?This separation allows request-scoped identity and process-level tenant infrastructure to remain independent while still participating in the same runtime flow.
Responsibilities
MultiTenantManager coordinates four primary concerns:
- tenant registration,
- tenant lookup,
- dynamic tenant discovery,
- MongoDB client lifecycle.
It also exposes runtime information such as the number of registered, connected, and lazy tenants.
A registered tenant can exist without having an active MongoDB connection.
This distinction is central to Ambiten's lazy tenancy model.
registered
≠
connectedA tenant may therefore be known to the runtime while its MongoDB client remains unopened until the first database operation requires it.
Tenant Configuration
A tenant configuration represents the infrastructure information Ambiten needs in order to work with a tenant.
Conceptually:
interface TenantConfig {
tenantId: string;
uri: string;
dbName: string;
client?: MongoClient;
lazy: boolean;
metadata?: Record<string, unknown>;
}For example:
const tenant = {
tenantId: "tenant5",
uri: "mongodb://localhost:27017",
dbName: "db_tenant5",
lazy: true,
metadata: {
region: "de-west-1",
tier: "supreme"
}
};The important distinction is that this configuration belongs to the runtime registry.
It is not request-scoped state.
It is not request-scoped state.
A request may carry:
{
tenantId: "tenant5"
}while MultiTenantManager holds the infrastructure required to actually reach that tenant.
getTenant()
MultiTenantManager.getTenant(tenantId)getTenant() performs a synchronous lookup against the current local tenant registry.
It does not perform external tenant discovery.
const tenant =
MultiTenantManager.getTenant("tenant5");If "tenant5" is already registered, the corresponding tenant configuration is returned.
If it is not registered, no external resolver is consulted.
Think of getTenant() as:
local registry lookup onlyThis makes it suitable when you specifically want to inspect the runtime's currently known tenant state.
For example:
const tenant =
MultiTenantManager.getTenant("tenant5");
if (tenant) {
console.log(tenant.dbName);
}When to use getTenant()
Use it when you want to:
- inspect a tenant that should already be registered,
- read runtime tenant metadata,
- examine lazy or connected state,
- avoid triggering external discovery.
Do not use it when your application needs to discover tenants dynamically.
For that, use resolveTenant().
resolveTenant()
await MultiTenantManager.resolveTenant(tenantId)resolveTenant() is the runtime-aware tenant lookup operation.
It first checks the local registry.
If the tenant is not registered and a dynamic tenant configuration resolver is available, Ambiten can use that resolver to discover the tenant externally.
Conceptually:
resolveTenant("tenant5")
↓
registered locally?
┌────┴────┐
yes no
↓ ↓
return TenantConfigResolver
config ↓
found?
┌────┴────┐
yes no
↓ ↓
register unresolved
↓
return configExample:
const tenant =
await MultiTenantManager.resolveTenant("tenant5");
if (!tenant) {
throw new Error("Tenant not found.");
}This is the correct operation when the tenant may exist outside the current runtime registry.
Local and dynamic resolution
The distinction between the two lookup methods is intentional:
getTenant()
= local registry only
resolveTenant()
= local registry
+
external discovery when requiredThis allows callers to be explicit about whether tenant discovery is permitted.
getClient()
await MultiTenantManager.getClient(tenantId)getClient() resolves the tenant and guarantees that a usable MongoDB client is available.
It is the resource-oriented operation in the manager.
Conceptually:
getClient("tenant5")
↓
resolve tenant
↓
tenant found?
↓
client already connected?
┌────┴────┐
yes no
↓ ↓
return establish client
client ↓
update runtime state
↓
return clientExample:
const client =
await MultiTenantManager.getClient("tenant5");
const db =
client.db("db_tenant5");In normal Ambiten model operations, application code usually does not need to call getClient() directly.
Ambiten's runtime uses the active tenant context to resolve the appropriate database resource when a model operation requires it.
For example:
const user = await UserModel.create({
username: "Abinod Ltd",
email: "aemma@abinod.com"
});If the active context contains:
{
tenantId: "tenant5"
}the runtime can resolve the correct tenant client without requiring the application to manually call:
MultiTenantManager.getClient("tenant5");Direct client access is therefore mainly useful for infrastructure-level operations or advanced integrations.
Lazy Tenant Activation
Ambiten separates tenant registration from MongoDB connection establishment.
Consider an application initialized with three tenants in lazy mode.
At startup:
MultiTenantManager.getStats();might return:
{
registeredTenants: 3,
connectedTenants: 0,
lazyTenants: 3,
dynamicResolverEnabled: true
}The tenants are known, but no tenant-specific MongoDB clients are active.
Now suppose a request arrives for a tenant that is not yet registered:
x-tenant-id: tenant5The adapter resolves:
AmbitenContext.get().tenantId;
// "tenant5"When a model operation requires database access:
tenant5
↓
not registered locally
↓
TenantConfigResolver
↓
configuration found
↓
tenant5 registered
↓
getClient()
↓
MongoDB client establishedThe tenant may then appear as:
{
tenantId: "tenant5",
dbName: "db_tenant5",
connected: true,
lazy: false,
metadata: {
region: "de-west-1",
tier: "supreme"
}
}while runtime statistics become:
{
registeredTenants: 4,
connectedTenants: 1,
lazyTenants: 3,
dynamicResolverEnabled: true
}The three unused tenants remain lazy.
Only the tenant that required a database operation has transitioned to an active connection.
Dynamic Tenant Configuration
Dynamic tenants are resolved through a TenantConfigResolver.
This allows tenant infrastructure to live outside the running application.
For example:
MultiTenantManager.setTenantConfigResolver({
async resolve(tenantId) {
const config =
await lookupTenant(tenantId);
if (!config) {
return undefined;
}
return {
tenantId,
uri: config.uri,
dbName: config.dbName,
lazy: true,
metadata: config.metadata
};
}
});The external lookup can be backed by any application-defined source, such as:
- a control database,
- a tenant registry service,
- an internal configuration API,
- an infrastructure service,
- a secrets-backed configuration store.
Ambiten only needs the resolver to produce the tenant configuration required by the runtime.
A dynamically discovered tenant then enters the same registry as tenants that were configured at startup.
const tenant =
await MultiTenantManager.resolveTenant(
"tenant5"
);At this point, if resolution succeeds, the tenant is known to the runtime.
A client can then be requested:
const client =
await MultiTenantManager.getClient(
"tenant5"
);The first operation answers:
Does this tenant exist, and what is its configuration?
The second answers:
Give me a usable MongoDB client for this tenant.
getAllTenants()
MultiTenantManager.getAllTenants()Returns the tenants currently registered in the runtime.
This includes tenants that may not yet have active MongoDB clients.
Conceptually:
Runtime Registry
tenant1 registered lazy
tenant2 registered lazy
tenant3 registered lazy
tenant5 registered connectedgetAllTenants() represents the registry as a whole, not just active connections.
It is useful for runtime inspection, diagnostics, administration, and observability.
getAllConnectedTenants()
MultiTenantManager.getAllConnectedTenants()Returns only tenants that currently have active clients.
For example:
tenants: 4
Connected tenants: 1could mean:
tenant1 lazy
tenant2 lazy
tenant3 lazy
tenant5 connectedCalling:
MultiTenantManager.getAllConnectedTenants();would therefore return only the connected tenant state.
This is useful when application infrastructure needs to reason specifically about active database resources rather than every registered tenant.
getStats()
MultiTenantManager.getStats()Returns a summary of the current multi-tenant runtime state.
For example:
const stats =
MultiTenantManager.getStats();
console.log(stats);{
registeredTenants: 4,
connectedTenants: 1,
lazyTenants: 3,
dynamicResolverEnabled: true
}These values describe different aspects of the runtime:
| Field | Meaning |
|---|---|
registeredTenants | Number of tenants currently known to the runtime |
connectedTenants | Number of tenants with active MongoDB clients |
lazyTenants | Number of registered tenants still waiting for on-demand connection |
dynamicResolverEnabled | Whether external tenant discovery is configured |
This makes getStats() useful for:
- runtime diagnostics,
- health endpoints,
- operational dashboards,
- development inspection,
- observability integrations.
getTenant(), resolveTenant(), and getClient()
These methods deliberately represent different levels of responsibility.
getTenant()
↓
local registry lookup
resolveTenant()
↓
local registry
+
dynamic discovery
getClient()
↓
tenant resolution
+
connection guaranteeOr more directly:
getTenant()
≠ external discovery
resolveTenant()
= registry + external discovery
getClient()
= resolution + usable MongoDB clientChoosing the correct method depends on what the caller actually needs.
Inspect local state
const tenant =
MultiTenantManager.getTenant("tenant5");Use this when the tenant should already exist in the runtime.
Discover a tenant if necessary
const tenant =
await MultiTenantManager.resolveTenant(
"tenant5"
);Use this when the tenant may need to be resolved externally.
Require database access
const client =
await MultiTenantManager.getClient(
"tenant5"
);Use this when a live tenant client is required.
MultiTenantManager vs AmbitenContext
MultiTenantManager and AmbitenContext solve different problems.
AmbitenContext Solves
AmbitenContext describes the current execution.
AmbitenContext.get();might return:
{
tenantId: "tenant5",
requestId: "req_123",
dbName: undefined,
collectionName: undefined
}Its question is:
What belongs to this request or operation?
MultiTenantManager Solves
MultiTenantManager describes tenant infrastructure known to the process.
MultiTenantManager.getTenant("tenant5");might represent:
{
tenantId: "tenant5",
uri: "...",
dbName: "db_tenant5",
lazy: false,
metadata: {
region: "de-west-1"
}
}Its question is:
What configuration and resources belong to this tenant?
The distinction is:
AmbitenContext
→ execution-scoped state
MultiTenantManager
→ runtime tenant stateA request can therefore carry only:
tenantId = tenant5while the manager knows:
tenant5
├── MongoDB URI
├── database name
├── connection state
├── lazy state
└── metadataThe request does not need to carry infrastructure configuration.
HTTP Request
↓
Framework Adapter
↓
TenantResolver
↓
tenantId = tenant5
↓
AmbitenContext
↓
Application
↓
UserModel.create(...)
↓
AmbitenClient
↓
MultiTenantManager
↓
resolveTenant("tenant5")
↓
getClient("tenant5")
↓
db_tenant5
↓
MongoDB operationApplication code remains focused on the operation:
await UserModel.create({
username: "Abinod Ltd",
email: "aemma@abinod.com"
});rather than manually coordinating:
tenant lookup
+
tenant registration
+
client creation
+
database selection
+
request contextThat coordination belongs to the runtime.
Using the Resolved Tenant Identity
Once a framework adapter has resolved the request's tenant, AmbitenContext should be treated as the runtime source of that execution identity.
For example:
const { tenantId } =
AmbitenContext.get();
if (!tenantId) {
throw new Error(
"Tenant context is required."
);
}This is preferable to repeatedly reading the original transport-specific value:
req.headers["x-tenant-id"];because the tenant may have been resolved from another source.
For example:
header
cookie
route parameter
subdomain
token claim
custom resolverAll of these can ultimately result in:
AmbitenContext.get().tenantId;The rest of the application can therefore remain independent of the original request mechanism.
Application Code Usually Does Not Call the Manager Per Request
Although MultiTenantManager is publicly available, ordinary request handlers generally do not need to perform tenant management manually.
You normally do not need this:
app.post("/users", async (req, res) => {
const tenantId =
AmbitenContext.get().tenantId;
await MultiTenantManager
.resolveTenant(tenantId);
await MultiTenantManager
.getClient(tenantId);
const user =
await UserModel.create(req.body);
res.json(user);
});Instead:
app.post("/users", async (req, res) => {
const user =
await UserModel.create(req.body);
res.json(user);
});The active Ambiten context supplies the tenant identity and the runtime handles resource resolution when the model operation requires it.
Direct manager access is most useful for:
- tenant administration,
- diagnostics,
- infrastructure services,
- explicit tenant preloading,
- runtime inspection,
- advanced integration code.
Avoid Mixing Request Identity and Tenant Infrastructure
A tenant identifier and a tenant configuration are not the same thing.
This:
{
tenantId: "tenant5"
}is enough to identify the tenant within an execution.
This:
{
tenantId: "tenant5",
uri: "...",
dbName: "db_tenant5",
lazy: true,
metadata: {
region: "de-west-1"
}
}belongs to tenant infrastructure.
Avoid propagating database URIs, connection state, or infrastructure metadata through application request layers.
Use:
AmbitenContextfor execution identity and:
MultiTenantManagerfor tenant resources.
Operational Model
A useful mental model is:
PROCESS
│
MultiTenantManager
│
┌───────────────┼───────────────┐
│ │ │
tenant1 tenant2 tenant5
lazy lazy connected
│
db_tenant5
REQUEST
│
AmbitenContext
│
tenantId=tenant5
│
model operation
│
└──────────────→ tenant5MultiTenantManager exists across requests.
AmbitenContext exists for the active execution.
That separation is what allows one application process to safely coordinate many tenant resources while each request retains its own tenant identity.
Related Pages
Continue with:
- Multi-Tenancy Overview — the overall Ambiten tenancy architecture.
- Tenant Resolution — how a request becomes a tenant identity.
- Dynamic Tenants — resolving tenants that are not registered at startup.
- Framework Adapters — connecting framework request lifecycles to Ambiten's runtime context.
For exact method signatures, parameter types, and generated declarations, see the MultiTenantManager API reference.
