Manage multiple Prisma databases in one app. No restart. No schema merge. Just db.get('name').
FlexiDB is a lightweight, type-safe abstraction layer on top of Prisma (v6 and v7+) that lets you manage multiple independent databases within a single Node.js application — without merging schemas or restarting your server.
import { createFlexiDB } from 'flexidb';
const db = createFlexiDB({
user: new UserPrismaClient({ adapter: userAdapter }),
analytics: new AnalyticsPrismaClient({ adapter: analyticsAdapter }),
orders: new OrderPrismaClient({ adapter: ordersAdapter }),
}, {
retry: { attempts: 3, backoff: 'exponential' },
circuitBreaker: { threshold: 5, resetTimeout: 30000 },
healthCheck: { interval: 30000, onStatusChange: (name, ok) => console.log(name, ok) },
reconnect: { enabled: true, interval: 5000, maxAttempts: 10 },
});
// Query any database
const users = await db.get('user').user.findMany();
// Run operations in parallel across multiple databases
const results = await db.batch({
user: (c) => c.user.findMany(),
analytics: (c) => c.event.count(),
});
// → { user: [...], analytics: 42 }
// Health check
const health = await db.health();
// → { user: true, analytics: true, orders: false }Prisma is powerful, but it only supports one database per schema. If your app talks to multiple databases — SaaS multi-tenancy, microservices, data lakes, legacy integrations — you're stuck merging schemas or manually managing client instances.
FlexiDB solves this cleanly:
| Without FlexiDB | With FlexiDB |
|---|---|
One schema.prisma for everything |
Separate .prisma per database |
| Manual client lifecycle | Auto-connect, auto-disconnect |
| No unified API | db.get('name') from anywhere |
| No health checks | db.health() + db.stats() built-in |
| No retry on connection failure | Auto-retry with exponential backoff |
| No resilience on repeated failures | Circuit breaker built-in |
| No reconnection after network blip | Auto-reconnect in background |
| Static database registration | db.register() / db.unregister() at runtime |
Manual db.ts setup |
npx flexidb codegen auto-generates it |
| No parallel query helper | db.batch() runs operations in parallel |
| No query interceptor | db.use() middleware for logging, metrics |
| Hard to unit test | createMockFlexiDB from flexidb/testing |
- Multi-database support — register as many databases as you need
- Full Prisma compatibility — works with Prisma v6 (URL-based) and v7+ (adapter-based)
- Read/Write splitting — pass
{ read, write }client pairs for primary/replica setups - Batch operations —
db.batch()runs parallel queries across multiple databases - Middleware —
db.use()intercepts all batch operations for logging, metrics, and audit trails - Circuit breaker — automatically open/close per-database circuits to prevent cascade failures
- Auto-reconnect — background reconnection when a connection drops mid-session
- Periodic health check — automatic interval-based monitoring with
onStatusChangecallback - Database groups —
db.group()+db.healthGroup()for bulk operations on subsets - CLI scaffolding —
npx flexidb initandnpx flexidb codegento get started in seconds - CLI tool — run
prisma generate,migrate,studio,seed,diff,watch, and more across all schemas - Dynamic registration —
db.register()/db.unregister()for multi-tenant, runtime databases - Connection retry — automatic reconnection with exponential backoff
- Event hooks —
onConnect,onDisconnect,onError,onRetrylifecycle callbacks - Enhanced statistics —
db.stats()with query counts, failure rates, and response times - Test utilities —
createMockFlexiDBfromflexidb/testingfor unit testing - Graceful shutdown — auto-disconnects on
SIGINT/SIGTERM - Type-safe — full TypeScript support with
.d.tsdeclarations
- Node.js 18+
- TypeScript 5+ (optional but recommended)
- Prisma v6 or v7+ installed in your project
# npm
npm install flexidb
# yarn
yarn add flexidb
# pnpm
pnpm add flexidbNote: Prisma is a peer dependency. Make sure you have
prismaand@prisma/clientinstalled in your project:npm install -D prisma@latest @prisma/client@latest
If you're starting from scratch, run:
npx flexidb initThis creates prisma/schemas/, an example schema, and .env.example — then tells you exactly what to do next.
FlexiDB expects your Prisma schemas at prisma/schemas/<name>.prisma.
your-project/
└── prisma/
└── schemas/
├── user.prisma
└── analytics.prisma
// prisma/schemas/user.prisma
generator client {
provider = "prisma-client" // Prisma v7+. Use "prisma-client-js" for v6 and below.
output = "../../src/generated/user"
}
datasource db {
provider = "mysql"
// Prisma v7+: no `url` here. Connection is passed via adapter in src/db.ts.
// Prisma v6: url = env("DATABASE_URL_USER")
}
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}// prisma/schemas/analytics.prisma
generator client {
provider = "prisma-client" // Prisma v7+. Use "prisma-client-js" for v6 and below.
output = "../../src/generated/analytics"
}
datasource db {
provider = "mysql"
}
model Event {
id Int @id @default(autoincrement())
action String
createdAt DateTime @default(now())
}Prisma v7+ note: The generator
provideris"prisma-client"and theurlfield is no longer set in the schema. Connection URLs are passed via a Driver Adapter at runtime. See Connecting to Databases.Prisma v6 note: Use
provider = "prisma-client-js"and seturl = env("DATABASE_URL")in the datasource block.
npx flexidb generateThis runs prisma generate for every schema in prisma/schemas/ and outputs clients to the paths defined in your output fields.
npx flexidb codegenThis reads your schemas, detects their output paths, and generates a ready-to-edit src/db.ts:
// src/db.ts — auto-generated by: npx flexidb codegen
import { createFlexiDB } from 'flexidb';
import { PrismaClient } from './generated/analytics/client';
import { PrismaClient as UserPrismaClient } from './generated/user/client';
export const db = createFlexiDB({
analytics : new PrismaClient({ adapter: null as any }), // TODO: replace with your adapter
user : new UserPrismaClient({ adapter: null as any }), // TODO: replace with your adapter
});Replace null as any with your real adapters. See Connecting to Databases.
import { db } from './db';
// Query user database
const user = await db.get('user').user.findUnique({ where: { id: 1 } });
// Query analytics database
await db.get('analytics').event.create({ data: { action: 'login' } });How you pass the connection URL depends on your Prisma version.
Prisma v7 introduced Driver Adapters. The connection URL is passed to the adapter, not the schema.
Install the adapter for your database:
# MySQL
npm install @prisma/adapter-mysql mysql2
# PostgreSQL
npm install @prisma/adapter-pg pg
# SQLite / Turso
npm install @prisma/adapter-libsql @libsql/clientThen configure the adapter:
// MySQL example
import { PrismaMysql } from '@prisma/adapter-mysql';
import { createPool } from 'mysql2';
const userAdapter = new PrismaMysql(
createPool({ uri: process.env.DATABASE_URL_USER })
);
export const db = createFlexiDB({
user: new UserPrismaClient({ adapter: userAdapter }),
});// PostgreSQL example
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
const analyticsAdapter = new PrismaPg(
new Pool({ connectionString: process.env.DATABASE_URL_ANALYTICS })
);
export const db = createFlexiDB({
analytics: new AnalyticsPrismaClient({ adapter: analyticsAdapter }),
});Pass the connection URL directly via datasourceUrl in the client constructor:
export const db = createFlexiDB({
user: new UserPrismaClient({
datasourceUrl: process.env.DATABASE_URL_USER,
}),
analytics: new AnalyticsPrismaClient({
datasourceUrl: process.env.DATABASE_URL_ANALYTICS,
}),
});Or set it in the schema:
datasource db {
provider = "mysql"
url = env("DATABASE_URL_USER")
}If you already have a Prisma project, FlexiDB integrates without breaking your existing setup.
npm install flexidbIf you currently have a single prisma/schema.prisma, you can keep it as-is and only use FlexiDB for your additional databases. Or move all schemas into prisma/schemas/:
mkdir -p prisma/schemas
mv prisma/schema.prisma prisma/schemas/main.prismaUpdate the output path in each schema's generator block:
generator client {
provider = "prisma-client" // v7+, or "prisma-client-js" for v6
output = "../../src/generated/main" // adjust relative to schema location
}npx flexidb generateimport { createFlexiDB } from 'flexidb';
import { PrismaClient as MainClient } from './generated/main/client';
import { PrismaClient as LegacyClient } from './generated/legacy/client';
export const db = createFlexiDB({
main: new MainClient({ adapter: mainAdapter }),
legacy: new LegacyClient({ adapter: legacyAdapter }),
});The flexidb CLI runs Prisma commands across all schemas (or a specific one) in prisma/schemas/.
npx flexidb <command> [subcommand] [options]
| Flag | Description |
|---|---|
--schema <name> |
Run for a specific schema only. Omit to run for all schemas. |
--output <path> |
Output path for codegen (default: src/db.ts) |
--force |
Overwrite existing file (used with codegen) |
Scaffolds a new FlexiDB project. Creates prisma/schemas/, an example schema, and .env.example.
npx flexidb initReads all schemas in prisma/schemas/, parses their output paths, and generates a ready-to-edit src/db.ts with all imports and createFlexiDB wired up.
npx flexidb codegen
npx flexidb codegen --output src/database.ts
npx flexidb codegen --force # overwrite existing fileGenerates Prisma clients for all schemas.
npx flexidb generate
npx flexidb generate --schema userCreates a new migration and applies it. Interactive — prompts for migration name.
npx flexidb migrate dev
npx flexidb migrate dev --schema userApplies all pending migrations. Use in CI/CD and production.
npx flexidb migrate deploynpx flexidb migrate reset --schema userShows migration status for each schema.
npx flexidb migrate statusWraps prisma migrate diff for each schema. Pass additional flags directly.
npx flexidb diff --schema user --from-schema-datamodel prisma/schemas/user.prisma --to-empty
npx flexidb diff --schema user --from-migrations prisma/user/migrations --to-schema-datamodel prisma/schemas/user.prismaOpens Prisma Studio in the browser for a specific schema. Only one schema at a time.
npx flexidb studio --schema user
npx flexidb studio # opens the first schema foundPushes the current schema state to the database without creating migration files. Useful for prototyping.
npx flexidb db push
npx flexidb db push --schema analytics.prisma file.
npx flexidb db pull --schema legacyValidates all schema files.
npx flexidb validateFormats all schema files.
npx flexidb formatRuns seed files from prisma/seeds/. Each schema can have its own seed file named <schema>.ts or <schema>.js.
npx flexidb seed # seed all schemas
npx flexidb seed --schema userSeed file convention: Create prisma/seeds/<schema>.ts (or .js):
// prisma/seeds/user.ts
import { PrismaClient } from '../src/generated/user/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.createMany({
data: [
{ email: 'alice@example.com' },
{ email: 'bob@example.com' },
],
});
}
main().finally(() => prisma.$disconnect());TypeScript seed files are run with ts-node. JavaScript files are run with node.
Watches prisma/schemas/*.prisma for changes and automatically re-runs npx flexidb generate when a schema is modified.
npx flexidb watchPress Ctrl+C to stop. Useful during active schema development.
Displays all available commands.
npx flexidb helpCreates a managed multi-database instance.
import { createFlexiDB } from 'flexidb';
const db = createFlexiDB(
{
user: new UserPrismaClient({ adapter }),
orders: new OrdersPrismaClient({ adapter }),
},
{
retry: {
attempts: 3, // default: 3
delay: 1000, // ms between retries, default: 1000
backoff: 'exponential' // 'fixed' | 'exponential', default: 'exponential'
},
hooks: {
onConnect: (name) => console.log(`✅ ${name} connected`),
onDisconnect: (name) => console.log(`🔌 ${name} disconnected`),
onError: (name, err) => console.error(`❌ ${name}:`, err),
onRetry: (name, attempt) => console.warn(`🔄 Retrying ${name} (${attempt})`),
},
circuitBreaker: {
threshold: 5, // failures before opening. Default: 5
resetTimeout: 30000, // ms before half-open retry. Default: 30000
},
healthCheck: {
interval: 30000, // ms between automatic health checks
onStatusChange: (name, isHealthy) => {
if (!isHealthy) alertTeam(name);
},
},
reconnect: {
enabled: true,
interval: 5000, // ms between reconnect attempts. Default: 5000
maxAttempts: 10, // give up after N attempts. Default: 10
},
}
);Returns: a FlexiDB instance with auto-connect, graceful shutdown, and all configured features built-in.
Returns the Prisma client for the given database name. Fully typed.
const userClient = db.get('user');
const users = await userClient.user.findMany();For read/write split databases, pass 'read' or 'write' as the second argument:
// Read from replica
const users = await db.get('user', 'read').user.findMany();
// Write to primary
await db.get('user', 'write').user.create({ data: { email: 'x@example.com' } });Throws if the database name does not exist.
Runs operations against multiple databases in parallel. Each key must match a registered database name.
const results = await db.batch({
user: (c) => c.user.findMany(),
analytics: (c) => c.event.count(),
orders: (c) => c.order.findMany({ where: { status: 'pending' } }),
});
// → { user: [...], analytics: 42, orders: [...] }batch() integrates with middleware, circuit breakers, and stats tracking automatically.
Registers a middleware function that wraps every batch() operation. Middlewares are called in registration order.
// Logging middleware
db.use(async (dbName, operation, next) => {
const start = Date.now();
const result = await next();
console.log(`[${dbName}] ${operation} took ${Date.now() - start}ms`);
return result;
});
// Multiple middlewares are chained
db.use(async (dbName, operation, next) => {
console.log(`[audit] ${dbName}:${operation}`);
return next();
});Creates a named group of databases for bulk operations.
db.group('tenants', ['tenant_acme', 'tenant_xyz']);
db.group('analytics', ['events', 'metrics']);Runs a health check only for databases in a named group.
const status = await db.healthGroup('tenants');
// → { tenant_acme: true, tenant_xyz: false }Returns an array of all registered database names (including dynamically registered ones).
db.list(); // ['user', 'orders', 'analytics']Checks connectivity for each registered database by running SELECT 1. Returns a map of name → boolean.
const status = await db.health();
// { user: true, orders: true, analytics: false }Returns a snapshot of per-database statistics.
const stats = db.stats();
// {
// user: {
// connected: true,
// registeredAt: Date,
// lastError: null,
// lastConnectedAt: Date,
// totalQueries: 1420,
// failedQueries: 3,
// avgResponseTimeMs: 12,
// }
// }
totalQueries,failedQueries, andavgResponseTimeMsare tracked for operations run throughdb.batch().
Registers a new database client at runtime — without restarting the app. Essential for multi-tenant architectures.
import { PrismaClient as TenantClient } from './generated/tenant/client';
db.register('tenant_acme', new TenantClient({ adapter: acmeAdapter }));
// Use it immediately
const orders = await db.get('tenant_acme').order.findMany();Also accepts a { read, write } pair for read/write splitting:
db.register('reporting', {
write: new ReportingClient({ adapter: primaryAdapter }),
read: new ReportingClient({ adapter: replicaAdapter }),
});Connects with retry automatically. Throws if a client with the same name is already registered.
Disconnects and removes a database client at runtime.
await db.unregister('tenant_acme');Manually disconnects all database clients and stops all background timers (health check, reconnect).
await db.disconnect();Note: FlexiDB automatically disconnects all clients on
SIGINTandSIGTERM. You usually don't need to call this manually.
For primary/replica setups, pass a { write, read } pair instead of a single client:
import { createFlexiDB } from 'flexidb';
const db = createFlexiDB({
user: {
write: new UserPrismaClient({ adapter: primaryAdapter }),
read: new UserPrismaClient({ adapter: replicaAdapter }),
},
});
// Route reads to replica
const users = await db.get('user', 'read').user.findMany();
// Route writes to primary
await db.get('user', 'write').user.create({ data: { email: 'x@example.com' } });Health checks, stats, and reconnect all apply to the write (primary) client.
Use db.register() and db.unregister() to dynamically add and remove tenant databases at runtime — no restart needed.
import { createFlexiDB } from 'flexidb';
import { PrismaClient as TenantClient } from './generated/tenant/client';
import { PrismaMysql } from '@prisma/adapter-mysql';
import { createPool } from 'mysql2';
export const db = createFlexiDB({
core: new CorePrismaClient({ adapter: coreAdapter }),
});
export async function onTenantCreated(tenantId: string, dbUrl: string) {
const adapter = new PrismaMysql(createPool({ uri: dbUrl }));
db.register(tenantId, new TenantClient({ adapter }));
}
const tenantId = req.headers['x-tenant-id'];
const tenantData = await db.get(tenantId).order.findMany();
export async function onTenantDeleted(tenantId: string) {
await db.unregister(tenantId);
}Import createMockFlexiDB from flexidb/testing to create a mock instance for unit tests. All health checks return true, and connect/disconnect are no-ops.
import { createMockFlexiDB } from 'flexidb/testing';
const db = createMockFlexiDB({
user: {
user: {
findMany: jest.fn().mockResolvedValue([{ id: 1, email: 'alice@example.com' }]),
create: jest.fn(),
},
},
});
// Use in tests exactly like the real db
const users = await db.get('user').user.findMany();
expect(users).toHaveLength(1);
// batch() also works
const results = await db.batch({
user: (c) => c.user.findMany(),
});After setup, your project should look like this:
your-project/
├── prisma/
│ ├── schemas/
│ │ ├── user.prisma
│ │ └── analytics.prisma
│ └── seeds/
│ ├── user.ts ← npx flexidb seed --schema user
│ └── analytics.ts
├── src/
│ ├── generated/ ← auto-generated by npx flexidb generate
│ │ ├── user/
│ │ └── analytics/
│ └── db.ts ← your createFlexiDB setup
└── .env
Add src/generated/ to your .gitignore:
src/generated
Store your connection URLs in .env:
DATABASE_URL_USER="mysql://user:password@localhost:3306/userdb"
DATABASE_URL_ANALYTICS="mysql://user:password@localhost:3306/analyticsdb"Reference them when setting up adapters:
const adapter = new PrismaMysql(
createPool({ uri: process.env.DATABASE_URL_USER! })
);FlexiDB is fully typed. The db.get('name') call returns the exact type of the client you registered, giving you full autocomplete and type safety.
// db is typed as FlexiDB<{ user: UserPrismaClient, analytics: AnalyticsPrismaClient }>
const db = createFlexiDB({
user: new UserPrismaClient({ adapter }),
analytics: new AnalyticsPrismaClient({ adapter }),
});
// db.get('user') returns UserPrismaClient ✅
// db.get('unknown') → TypeScript error ✅All types are exported and available for use in your own code:
import type {
FlexiDB, // the db instance type
FlexiDBOptions, // options for createFlexiDB
FlexiDBHooks, // lifecycle hook callbacks
RetryConfig, // retry configuration
ReconnectConfig, // auto-reconnect configuration
CircuitBreakerConfig, // circuit breaker configuration
HealthCheckConfig, // periodic health check configuration
MiddlewareFn, // middleware function signature
ReadWriteClient, // { read, write } pair type
DBStats, // per-database stats shape
} from 'flexidb';ISC — see LICENSE.
Built with ❤️ in Indonesia. Open source. Zero magic. Just TypeScript.