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 module | Default module name | Default connection export | Factory | Native driver |
|---|---|---|---|---|
db-mongo | db-mongo | dbMongo | createDbMongoModule | mongodb |
db-postgres | db-postgres | dbPostgres | createDbPostgresModule | pg |
db-mysql | db-mysql | dbMysql | createDbMysqlModule | mysql2/promise |
db-mariadb | db-mariadb | dbMariadb | createDbMariadbModule | mariadb |
db-sqlite | db-sqlite | dbSqlite | createDbSqliteModule | better-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.
| Adapter | Runtime exports | Type/error exports |
|---|---|---|
| MongoDB | dbMongo, dbMongoModule, createDbMongoModule | DbMongoOptions, DbMongoClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode |
| PostgreSQL | dbPostgres, dbPostgresModule, createDbPostgresModule | DbPostgresOptions, DbPostgresClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode |
| MySQL | dbMysql, dbMysqlModule, createDbMysqlModule | DbMysqlOptions, DbMysqlClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode |
| MariaDB | dbMariadb, dbMariadbModule, createDbMariadbModule | DbMariadbOptions, DbMariadbClient, DatabaseHealth, DatabaseState, DatabaseError, DatabaseErrorCode |
| SQLite | dbSqlite, dbSqliteModule, createDbSqliteModule | DbSqliteOptions, 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:
- validates the ExetJS project and Core compatibility;
- validates both registry marker zones before writing;
- follows custom
paths.modulesDirandpaths.moduleRegistryFilevalues; - copies only the selected standalone template;
- appends documented settings to the configured
.env.examplewithout overwriting existing values; - adds only the selected native driver’s dependencies;
- installs dependencies unless
--no-installis used; - does not modify the application’s
.envfile.
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:
| Code | Meaning |
|---|---|
DB_CONFIG_INVALID | Required configuration is missing or a value is invalid |
DB_CONNECT_FAILED | The native connection could not be opened or pass the initial health probe |
DB_NOT_READY | The native client was requested when the connection was not ready |
DB_HEALTH_TIMEOUT | A health probe exceeded the configured health timeout |
DB_CLOSE_FAILED | The 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
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_MONGO_URI | yes | none | Must start with mongodb:// or mongodb+srv:// |
DB_MONGO_DATABASE | yes | none | Explicit application database name |
DB_MONGO_POOL_SIZE | no | 10 | MongoClient.maxPoolSize |
DB_MONGO_CONNECT_TIMEOUT_MS | no | 10000 | Used for connect and server selection timeout |
DB_MONGO_HEALTH_TIMEOUT_MS | no | 5000 | Health 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
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_POSTGRES_URL | yes | none | Must use postgres: or postgresql: protocol |
DB_POSTGRES_POOL_SIZE | no | 10 | Maximum pool size |
DB_POSTGRES_CONNECT_TIMEOUT_MS | no | 10000 | Pool connection acquisition timeout |
DB_POSTGRES_HEALTH_TIMEOUT_MS | no | 5000 | Health 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
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_MYSQL_HOST | no | 127.0.0.1 | Database hostname |
DB_MYSQL_PORT | no | 3306 | Valid TCP port from 1 to 65535 |
DB_MYSQL_USER | yes | none | Application database user |
DB_MYSQL_PASSWORD | no | empty string | Use an external secret source in production |
DB_MYSQL_DATABASE | yes | none | Application database name |
DB_MYSQL_POOL_SIZE | no | 10 | Pool connection limit |
DB_MYSQL_CONNECT_TIMEOUT_MS | no | 10000 | Native connect timeout |
DB_MYSQL_HEALTH_TIMEOUT_MS | no | 5000 | Health query timeout |
DB_MYSQL_TLS | no | false | Enables 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
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_MARIADB_HOST | no | 127.0.0.1 | Database hostname |
DB_MARIADB_PORT | no | 3306 | Valid TCP port from 1 to 65535 |
DB_MARIADB_USER | yes | none | Application database user |
DB_MARIADB_PASSWORD | no | empty string | Use an external secret source in production |
DB_MARIADB_DATABASE | yes | none | Application database name |
DB_MARIADB_POOL_SIZE | no | 10 | Pool connection limit |
DB_MARIADB_CONNECT_TIMEOUT_MS | no | 10000 | Connect and acquisition timeout |
DB_MARIADB_HEALTH_TIMEOUT_MS | no | 5000 | Health query timeout |
DB_MARIADB_TLS | no | false | Enables 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
| Variable | Required | Default | Notes |
|---|---|---|---|
DB_SQLITE_FILENAME | yes at runtime | none in runtime config | Installer suggests ./app.sqlite in .env.example |
DB_SQLITE_BUSY_TIMEOUT_MS | no | 5000 | SQLite lock wait timeout |
DB_SQLITE_READONLY | no | false | Opens an existing DB in read-only mode when true |
DB_SQLITE_HEALTH_TIMEOUT_MS | no | 5000 | Common 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.
- PostgreSQL transactions should use one checked-out
pgclient. - MySQL transactions should use one acquired
mysql2/promiseconnection. - MariaDB transactions should use one acquired MariaDB connection.
- MongoDB transactions should use native sessions where supported by the deployment topology.
- SQLite transactions should use
better-sqlite3transaction APIs.
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:
- create application tables or collections;
- generate schemas;
- run migrations;
- provide an ORM;
- retry application writes automatically;
- translate queries between database engines.
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.