Browse documentation
DOCS/Database modules

Database modules

ExetJS 0.2.0 ships five standalone database connection templates. Each installed module is readable TypeScript inside the application and owns its native driver, configuration, typed client access, health checks and lifecycle. There is no published or consumer runtime DB Core package.

Full public documentation is available at https://exetjs.com/.

Available modules

CLI moduleDefault module nameDefault connection exportFactoryNative driver
db-mongodb-mongodbMongocreateDbMongoModulemongodb
db-postgresdb-postgresdbPostgrescreateDbPostgresModulepg
db-mysqldb-mysqldbMysqlcreateDbMysqlModulemysql2/promise
db-mariadbdb-mariadbdbMariadbcreateDbMariadbModulemariadb
db-sqlitedb-sqlitedbSqlitecreateDbSqliteModulebetter-sqlite3

The adapters share one connection contract, but they deliberately expose their native driver clients instead of introducing a universal query abstraction or mandatory ORM.

Public exports

Each installed adapter exports its default module, default connection object, factory, option type, native client type, lifecycle/health types and sanitized error types.

AdapterRuntime exportsType/error exports
MongoDBdbMongo, dbMongoModule, createDbMongoModuleDbMongoOptions, DbMongoClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode
PostgreSQLdbPostgres, dbPostgresModule, createDbPostgresModuleDbPostgresOptions, DbPostgresClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode
MySQLdbMysql, dbMysqlModule, createDbMysqlModuleDbMysqlOptions, DbMysqlClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode
MariaDBdbMariadb, dbMariadbModule, createDbMariadbModuleDbMariadbOptions, DbMariadbClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode
SQLitedbSqlite, dbSqliteModule, createDbSqliteModuleDbSqliteOptions, DbSqliteClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode

The repeated DatabaseHealth, DatabaseState, DatabaseError and DatabaseErrorCode names are local exports from each standalone generated module. There is no shared DB runtime dependency.

Requirements and installation

DB templates shipped with CLI 0.2.0 require @exetjs/core 0.2.0 or newer.

Inspect an installation without writing:

exet install db-postgres --dry-run

Install one adapter:

exet install db-postgres

The installer:

After installation, copy or provide the required values through your actual runtime environment before starting the application.

Common connection API

Every installed DB module exposes the same lifecycle surface:

await dbX.connect();
const client = dbX.getClient();
const health = await dbX.checkHealth();
const state = dbX.getState();
await dbX.disconnect();

connect()

Opens the native client/pool and performs an initial health probe. Concurrent lifecycle operations are serialized. Calling connect() while an already ready client exists is safe and returns without opening a second client.

An installed default DB module connects automatically when ExetJS registers that module. A missing required setting or unavailable database fails startup. There is no automatic fallback to memory.

getClient()

Returns the typed native client only in ready state. Otherwise it throws DatabaseError with code DB_NOT_READY.

Framework-managed DB modules own their native resource. Application code should not call the native client’s close/end method directly.

checkHealth()

Returns:

type DatabaseHealth = {
  ok: boolean;
  state: DatabaseState;
  durationMs: number;
};

Health probes are coalesced while one is already in progress. A failed or timed-out probe returns ok: false; it does not expose raw driver errors, credentials, SQL or connection strings.

getState() reports lifecycle state, not guaranteed network availability. Use checkHealth().ok when current readiness matters.

disconnect()

Closes the managed resource. Repeated calls are safe. A later explicit connect() creates a new client or pool.

Lifecycle states

The exported DatabaseState type is:

type DatabaseState =
  "idle" | "connecting" | "ready" | "closing" | "closed" | "failed";

Typical lifecycle:

idle -> connecting -> ready -> closing -> closed

A startup or close failure moves the connection to failed. If startup fails after a candidate native resource was created, the module attempts to close that candidate before returning the sanitized error.

Error model

Every adapter exports its own DatabaseError class and DatabaseErrorCode type with the same codes:

CodeMeaning
DB_CONFIG_INVALIDRequired configuration is missing or a value is invalid
DB_CONNECT_FAILEDThe native connection could not be opened or pass the initial health probe
DB_NOT_READYThe native client was requested when the connection was not ready
DB_HEALTH_TIMEOUTA health probe exceeded the configured health timeout
DB_CLOSE_FAILEDThe native resource could not be closed cleanly

DatabaseError is intentionally safe to log. Raw driver errors can contain credentials, SQL, URLs or other sensitive data, so connection and close failures are converted to sanitized module errors.

Normal application query errors still come from the selected native driver and propagate to application code.

Configuration precedence

For module configuration, explicit factory options override environment variables, which override built-in defaults where defaults exist:

factory options > environment variables > module defaults

Required values without defaults still fail with DB_CONFIG_INVALID.

Native driverOptions are applied first and ExetJS connection-critical options are then applied explicitly. This ensures fields such as host, database, pool size and timeout match the ExetJS module configuration. MySQL and MariaDB preserve native driverOptions, including ssl. Setting the ExetJS tls option to true adds certificate-verified TLS when driverOptions.ssl is not already supplied.

Startup dependencies

When another module needs a DB during registration, declare the dependency explicitly:

import type { ExetModule } from "@exetjs/core";
import { dbPostgres } from "../db-postgres/index.js";

export const repositoryModule: ExetModule = {
  name: "repositories",
  dependsOn: ["db-postgres"],
  register() {
    const pool = dbPostgres.getClient();
    void pool;
    return {};
  },
};

ExetJS validates missing dependencies, duplicate module names and cycles before opening resources. Dependencies register before dependants regardless of array order. Startup failure disposes successfully registered resources in reverse registration order.

Multiple connections of the same provider

Each factory call owns one independent connection. Give additional instances distinct module names:

import { createDbPostgresModule } from "./modules/db-postgres/index.js";

export const reportingDbModule = createDbPostgresModule(
  {
    url: process.env.REPORTING_DATABASE_URL!,
    poolSize: 5,
  },
  "db-postgres-reporting",
);

export const reportingDb = reportingDbModule.database;

A single module instance cannot belong to more than one ExetJS application context at the same time.


MongoDB

Install

exet install db-mongo

Native dependency:

mongodb

Environment variables

DB_MONGO_URI=mongodb://USER:PASSWORD@HOST:27017/?authSource=admin
DB_MONGO_DATABASE=app
DB_MONGO_POOL_SIZE=10
DB_MONGO_CONNECT_TIMEOUT_MS=10000
DB_MONGO_HEALTH_TIMEOUT_MS=5000
VariableRequiredDefaultNotes
DB_MONGO_URIyesnoneMust start with mongodb:// or mongodb+srv://
DB_MONGO_DATABASEyesnoneExplicit application database name
DB_MONGO_POOL_SIZEno10MongoClient.maxPoolSize
DB_MONGO_CONNECT_TIMEOUT_MSno10000Used for connect and server selection timeout
DB_MONGO_HEALTH_TIMEOUT_MSno5000Health deadline and wait queue timeout

Programmatic options

type DbMongoOptions = {
  uri: string;
  database: string;
  poolSize?: number;
  connectTimeoutMs?: number;
  healthTimeoutMs?: number;
  driverOptions?: MongoClientOptions;
};

Example:

const mongoModule = createDbMongoModule({
  uri: process.env.MONGO_URL!,
  database: "app",
  poolSize: 20,
  driverOptions: {
    appName: "my-api",
  },
});

Native client shape

MongoDB intentionally returns both the native MongoClient and the selected Db:

type DbMongoClient = {
  connection: MongoClient;
  db: Db;
};

Use the selected database directly:

import { dbMongo } from "./modules/db-mongo/index.js";

const users = dbMongo.getClient().db.collection("users");
const user = await users.findOne({ email: "jane@example.com" });

Use connection only when you need a MongoClient-level API:

const { connection } = dbMongo.getClient();

Health

Health uses the MongoDB ping command against the configured database.

TLS, replica-set, retry and authentication options can be supplied through the connection URI and typed MongoClientOptions. ExetJS does not weaken certificate verification automatically.


PostgreSQL

Install

exet install db-postgres

Native dependencies:

pg
@types/pg

Environment variables

DB_POSTGRES_URL=postgresql://USER:PASSWORD@HOST:5432/DATABASE
DB_POSTGRES_POOL_SIZE=10
DB_POSTGRES_CONNECT_TIMEOUT_MS=10000
DB_POSTGRES_HEALTH_TIMEOUT_MS=5000
VariableRequiredDefaultNotes
DB_POSTGRES_URLyesnoneMust use postgres: or postgresql: protocol
DB_POSTGRES_POOL_SIZEno10Maximum pool size
DB_POSTGRES_CONNECT_TIMEOUT_MSno10000Pool connection acquisition timeout
DB_POSTGRES_HEALTH_TIMEOUT_MSno5000Health query timeout

Programmatic options

type DbPostgresOptions = {
  url: string;
  poolSize?: number;
  connectTimeoutMs?: number;
  healthTimeoutMs?: number;
  driverOptions?: PoolConfig;
};

Native client

getClient() returns pg.Pool:

import { dbPostgres } from "./modules/db-postgres/index.js";

const result = await dbPostgres
  .getClient()
  .query("SELECT id, email FROM users WHERE id = $1", [id]);

const user = result.rows[0] ?? null;

Health checks acquire one pool client, run SELECT 1, then release it. Idle pool errors are handled by the adapter and logged through the ExetJS module logger without exposing the raw driver error.

Configure PostgreSQL TLS through the connection URL or typed PoolConfig in driverOptions according to the deployment environment.


MySQL

Install

exet install db-mysql

Native dependency:

mysql2

Environment variables

DB_MYSQL_HOST=127.0.0.1
DB_MYSQL_PORT=3306
DB_MYSQL_USER=app
DB_MYSQL_PASSWORD=
DB_MYSQL_DATABASE=app
DB_MYSQL_POOL_SIZE=10
DB_MYSQL_CONNECT_TIMEOUT_MS=10000
DB_MYSQL_HEALTH_TIMEOUT_MS=5000
DB_MYSQL_TLS=false
VariableRequiredDefaultNotes
DB_MYSQL_HOSTno127.0.0.1Database hostname
DB_MYSQL_PORTno3306Valid TCP port from 1 to 65535
DB_MYSQL_USERyesnoneApplication database user
DB_MYSQL_PASSWORDnoempty stringUse an external secret source in production
DB_MYSQL_DATABASEyesnoneApplication database name
DB_MYSQL_POOL_SIZEno10Pool connection limit
DB_MYSQL_CONNECT_TIMEOUT_MSno10000Native connect timeout
DB_MYSQL_HEALTH_TIMEOUT_MSno5000Health query timeout
DB_MYSQL_TLSnofalseEnables TLS with certificate verification

Programmatic options

type DbMysqlOptions = {
  host: string;
  user: string;
  password: string;
  database: string;
  port?: number;
  poolSize?: number;
  connectTimeoutMs?: number;
  healthTimeoutMs?: number;
  tls?: boolean;
  driverOptions?: PoolOptions;
};

Native client

getClient() returns mysql2/promise.Pool:

import { dbMysql } from "./modules/db-mysql/index.js";

const [rows] = await dbMysql
  .getClient()
  .execute("SELECT id, email FROM users WHERE id = ?", [id]);

TLS

When DB_MYSQL_TLS=true or tls: true, ExetJS supplies:

ssl: {
  rejectUnauthorized: true;
}

unless driverOptions.ssl is explicitly supplied. Use a typed native SSL configuration when your deployment requires custom CA material or other driver-specific TLS settings.

Health checks acquire one pool connection, run SELECT 1, then release it. A connection that fails during the health query is destroyed instead of being returned to the pool.


MariaDB

Install

exet install db-mariadb

Native dependency:

mariadb

Environment variables

DB_MARIADB_HOST=127.0.0.1
DB_MARIADB_PORT=3306
DB_MARIADB_USER=app
DB_MARIADB_PASSWORD=
DB_MARIADB_DATABASE=app
DB_MARIADB_POOL_SIZE=10
DB_MARIADB_CONNECT_TIMEOUT_MS=10000
DB_MARIADB_HEALTH_TIMEOUT_MS=5000
DB_MARIADB_TLS=false
VariableRequiredDefaultNotes
DB_MARIADB_HOSTno127.0.0.1Database hostname
DB_MARIADB_PORTno3306Valid TCP port from 1 to 65535
DB_MARIADB_USERyesnoneApplication database user
DB_MARIADB_PASSWORDnoempty stringUse an external secret source in production
DB_MARIADB_DATABASEyesnoneApplication database name
DB_MARIADB_POOL_SIZEno10Pool connection limit
DB_MARIADB_CONNECT_TIMEOUT_MSno10000Connect and acquisition timeout
DB_MARIADB_HEALTH_TIMEOUT_MSno5000Health query timeout
DB_MARIADB_TLSnofalseEnables TLS with certificate verification

Programmatic options

type DbMariadbOptions = {
  host: string;
  user: string;
  password: string;
  database: string;
  port?: number;
  poolSize?: number;
  connectTimeoutMs?: number;
  healthTimeoutMs?: number;
  tls?: boolean;
  driverOptions?: PoolConfig;
};

Native client

getClient() returns mariadb.Pool:

import { dbMariadb } from "./modules/db-mariadb/index.js";

const rows = await dbMariadb
  .getClient()
  .query("SELECT id, email FROM users WHERE id = ?", [id]);

TLS

When DB_MARIADB_TLS=true or tls: true, ExetJS supplies:

ssl: {
  rejectUnauthorized: true;
}

unless driverOptions.ssl is explicitly supplied.

The adapter attaches an error handler to newly created native connections so background connection errors are surfaced through the ExetJS logger without exposing raw driver error details in the module’s sanitized lifecycle errors.


SQLite

Install

exet install db-sqlite

Native dependencies:

better-sqlite3
@types/better-sqlite3

better-sqlite3 is a native dependency. Generated pnpm projects allow its build script. Existing projects must permit the package’s install/build script according to their package-manager policy. Platforms without a compatible prebuilt binary may require Node headers and C/C++ build tooling.

Environment variables

The template appends this suggested configuration to .env.example:

DB_SQLITE_FILENAME=./app.sqlite
DB_SQLITE_BUSY_TIMEOUT_MS=5000
DB_SQLITE_READONLY=false
DB_SQLITE_HEALTH_TIMEOUT_MS=5000
VariableRequiredDefaultNotes
DB_SQLITE_FILENAMEyes at runtimenone in runtime configInstaller suggests ./app.sqlite in .env.example
DB_SQLITE_BUSY_TIMEOUT_MSno5000SQLite lock wait timeout
DB_SQLITE_READONLYnofalseOpens an existing DB in read-only mode when true
DB_SQLITE_HEALTH_TIMEOUT_MSno5000Common health deadline setting

./app.sqlite is a generated example value, not a hidden runtime fallback. If DB_SQLITE_FILENAME is absent and no programmatic filename is supplied, configuration fails with DB_CONFIG_INVALID.

The parent directory for a file database must already exist. :memory: can be used for isolated tests.

Programmatic options

type DbSqliteOptions = {
  filename: string;
  busyTimeoutMs?: number;
  readonly?: boolean;
  healthTimeoutMs?: number;
};

Native client

getClient() returns better-sqlite3’s Database.Database:

import { dbSqlite } from "./modules/db-sqlite/index.js";

const user = dbSqlite
  .getClient()
  .prepare("SELECT id, email FROM users WHERE id = ?")
  .get(id);

The adapter enables:

PRAGMA foreign_keys = ON

It does not force a journal mode. Applications that need WAL or another mode should configure it explicitly.

SQLite queries are synchronous because that is the native better-sqlite3 API. The ExetJS lifecycle remains promise-based so SQLite follows the same module contract as network databases.


Transactions

ExetJS does not add a transaction abstraction.

Always release checked-out network connections in finally when using driver-level transaction APIs.

Schema, migrations and ORM boundary

DB modules provide connection lifecycle only. They do not:

Applications remain free to use native APIs, an ORM/query builder compatible with the selected native client, or application-specific repositories.

Auth integration

Installing a DB module does not silently change Auth storage. The auth starter remains in-memory until the application supplies an AuthRepository and declares the corresponding DB dependency.

See Auth module for the complete repository contract and wiring example.

Shutdown behavior

Resource-backed modules are disposed in reverse registration order. startExetServer handles SIGINT/SIGTERM by default when resource disposers are present, drains HTTP traffic, then disposes module resources within the configured shutdown deadline.

Use { handleSignals: false } when embedding ExetJS inside another process manager or implementing custom signal handling, and close resources through stopExetServer / stopExetModules rather than directly closing managed DB clients.

Updating installed templates

The CLI installs source templates into the application. Updating @exetjs/cli does not rewrite modules that were already installed.

Review changes before using:

exet install db-postgres --force

--force overwrites generated module files. Keep application repositories, migrations and domain code outside the generated DB connection module so template updates remain reviewable.

Driver references