DEV Community

Emmanue; Nodolo
Emmanue; Nodolo

Posted on

Ambiten v1.3.0: External Schemas, Runtime Defaults, and Tenant Resolver Modules

Ambiten Core v1.3.0 adds a practical way to keep bootstrap configuration manageable: reference schema definitions, model options, and tenant resolver functions from application modules.

It also introduces declarative runtime defaults and restores preparation of configured log files during startup.

This walkthrough shows how the configuration pieces fit together.

1. Install the release

In your application:

pnpm add @ambiten/core@1.3.0
Enter fullscreen mode Exit fullscreen mode

Use a Node.js version compatible with both Core and your MongoDB driver. Core v1.3.0 declares Node.js >=20.11.0 and a MongoDB peer range of ^5 || ^7.

The examples use native .mjs modules. Bootstrap initialization requires a running MongoDB server.

We will use these files:

File Purpose
ambiten.config.json Connects the configuration pieces
src/schemas/user.schema.mjs Exports the schema definition
src/models/user.model.mjs Exports model options
src/tenantResolver.mjs Resolves tenant database configuration

2. Export a schema definition

Create src/schemas/user.schema.mjs:

export const userSchema = {
  tenantId: {
    type: "string",
    required: true,
    default: {
      $runtime: "context",
      key: "tenantId"
    }
  },

  publicId: {
    type: "string",
    default: {
      $runtime: "uuid"
    }
  },

  username: {
    type: "string",
    required: true
  },

  email: {
    type: "string",
    required: true
  },

  createdAt: {
    type: "date",
    required: true,
    default: {
      $runtime: "now"
    }
  },

  isDeleted: {
    type: "boolean",
    default: false
  },

  deletedAt: {
    type: "date"
  }
};
Enter fullscreen mode Exit fullscreen mode

The export is a schema definition object. Bootstrap constructs the schema instance from it.

The three runtime defaults have different jobs:

Directive Resolved value
{ $runtime: "now" } A new Date
{ $runtime: "uuid" } A generated UUID
{ $runtime: "context", key: "tenantId" } The active context’s tenant identifier

Defaults apply to fields whose values are undefined.

Notice that deletedAt has no default. A new document starts with isDeleted: false and no deletion timestamp. A deletion workflow can assign a valid date later.

3. Export model options

Create src/models/user.model.mjs:

export const userModelOptions = {
  collectionName: "users"
};
Enter fullscreen mode Exit fullscreen mode

This export supplies the model options. Bootstrap combines them with the loaded schema and provider when constructing the model.

For this configuration path, export the options object rather than constructing a model inside the module.

4. Export a tenant configuration resolver

Create src/tenantResolver.mjs:

const tenantConfigs = new Map([
  [
    "tenant-a",
    {
      uri: "mongodb://localhost:27017/tenant_a",
      dbName: "tenant_a"
    }
  ],
  [
    "tenant-b",
    {
      uri: "mongodb://localhost:27017/tenant_b",
      dbName: "tenant_b"
    }
  ]
]);

export function resolveTenantConfig(tenantId) {
  return tenantConfigs.get(tenantId);
}
Enter fullscreen mode Exit fullscreen mode

These local connection strings make the example easy to follow.

In an application, the resolver can obtain configuration from an environment-backed mapping, control database, configuration API, or another trusted source. The function can be asynchronous.

The resolver contract accepts a tenant identifier and returns:

{
  uri: string;
  dbName?: string;
  metadata?: Record<string, unknown>;
}
Enter fullscreen mode Exit fullscreen mode

Returning undefined or null means the tenant could not be resolved.

The resolver supplies configuration. MultiTenantManager owns tenant registration and client state.

5. Connect the modules in JSON

Create ambiten.config.json in the project root:

{
  "configVersion": "1.0.0",
  "projectName": "ambiten-v130-demo",
  "connection": {
    "uri": "mongodb://localhost:27017/ambiten_demo",
    "options": {
      "dbName": "ambiten_demo"
    }
  },
  "schema": {
    "path": "./src/schemas/user.schema.mjs",
    "exportName": "userSchema"
  },
  "model": {
    "path": "./src/models/user.model.mjs",
    "exportName": "userModelOptions"
  },
  "multiTenant": {
    "enabled": true,
    "tenants": {},
    "tenantConfigResolverModule": {
      "path": "./src/tenantResolver.mjs",
      "exportName": "resolveTenantConfig"
    },
    "initOptions": {
      "lazy": true
    }
  },
  "logger": {
    "enabled": true,
    "level": "info",
    "transportConfigs": [
      {
        "type": "console",
        "options": {}
      },
      {
        "type": "rotating-file",
        "options": {
          "filename": "./logs/ambiten.log"
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

A few details matter here:

  • configVersion identifies the configuration format; it is independent of the Core package version.
  • Module paths in JSON resolve relative to the configuration file.
  • Omitting exportName selects the module’s default export.
  • An empty tenants object is valid when an external tenant resolver is configured.
  • Bootstrap still initializes the base connection. Lazy tenant registration does not remove that startup connection.

The features.models and features.schemas properties remain scaffolding metadata. Runtime loading uses the explicit model and schema references.

6. Initialize once and use the loaded model

Within a long-running application, initialize the runtime once and reuse it:

import {
  AmbitenBootstrapFactory,
  AmbitenContext
} from "@ambiten/core";

const runtime = await AmbitenBootstrapFactory.create({
  config: "./ambiten.config.json"
});

const UserModel = runtime.getModel();
Enter fullscreen mode Exit fullscreen mode

At an independent execution boundary, an operation can establish its tenant context explicitly:

const user = await AmbitenContext.run(
  {
    tenantId: "tenant-a",
    logger: runtime.getLogger()
  },
  async () => {
    return UserModel.create({
      username: "Alice",
      email: "alice@example.com"
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

For a successful creation, the schema supplies the missing tenant identifier, public UUID, creation timestamp, and isDeleted value.

The tenant resolver provides the connection configuration for tenant-a when that tenant is needed.

In an HTTP application, a framework adapter can establish the request context before the handler runs. The bootstrap configuration itself does not install HTTP middleware or start a server.

Use a tenant identifier that the application has authenticated and authorized. A tenant default fills a missing document field; it does not validate a caller’s right to access that tenant or overwrite a tenant value supplied in input.

For server applications, coordinate HTTP or worker shutdown and tenant client cleanup with the runtime’s lifecycle.

7. Understand when defaults run

Runtime defaults resolve when schema validation applies them to a missing field. They are not evaluated once and shared across all documents.

The supported context keys are:

tenantId
requestId
dbName
collectionName
Enter fullscreen mode Exit fullscreen mode

If a requested context value is unavailable, applying that default fails.

Other useful behaviors:

  • Unsupported runtime directives are rejected.
  • Defaults apply only to undefined values.
  • Explicit values remain subject to field validation.
  • Object and array defaults are copied per document.
  • Date fields require valid Date values when supplied.

Use literal JSON-compatible defaults or supported $runtime descriptors in both inline schemas and exported schema definitions.

8. Configure file logging explicitly

The example includes a rotating-file transport targeting:

./logs/ambiten.log
Enter fullscreen mode Exit fullscreen mode

For declarative file transports, bootstrap creates missing parent directories and prepares the destination file during logger initialization.

Relative log paths resolve from the process working directory. Existing file contents are preserved.

When logging is enabled and no transports are supplied, bootstrap uses its default console and rotating-file transports. An explicit console-only runtime configuration remains console-only.

Applications supplying their own logger instance or executable transports manage those transports themselves.

9. Using TypeScript modules

You can reference TypeScript files instead of .mjs files, provided the application process supports loading them through native imports.

For example, with tsx installed:

node --import tsx src/main.ts
Enter fullscreen mode Exit fullscreen mode

Then a module reference can use:

{
  "schema": {
    "path": "./src/schemas/user.schema.ts",
    "exportName": "userSchema"
  }
}
Enter fullscreen mode Exit fullscreen mode

A CommonJS require hook alone is insufficient for this native import path.

For compiled deployments, update configuration references to the emitted JavaScript files and ensure those files are deployed. TypeScript compilation does not rewrite module paths inside your JSON configuration.

Other changes in v1.3.0

The release also includes:

  • ESM reference fixes for Node.js module resolution.
  • CommonJS and ESM package integration fixes across Core, adapter types, adapter runtime, and the Express adapter.
  • Context propagation checks across those package boundaries.
  • Browser bundle build fixes involving Node.js imports.
  • More reliable Windows invocation of pnpm in build and integration scripts.
  • Scaffold summaries that understand the revised model configuration.
  • Generated applications requesting @ambiten/core through the registry’s latest tag.

The initial installation resolves that tag. The resulting lockfile records the selected dependency version.

Scaffold compatibility note: the v1.3.0 template generates MongoDB ^6.14.2, while Core declares ^5 || ^7. Align the generated driver dependency with Core’s peer range and the selected driver’s Node.js requirements.

Explore the release

Inline configuration remains supported. External modules give larger applications a way to organize definitions while continuing to use the same bootstrap workflow.

Top comments (0)