Skip to content

suryamsj/flexidb

Repository files navigation

FlexiDB

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 }

Why FlexiDB?

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

Features

  • 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 operationsdb.batch() runs parallel queries across multiple databases
  • Middlewaredb.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 onStatusChange callback
  • Database groupsdb.group() + db.healthGroup() for bulk operations on subsets
  • CLI scaffoldingnpx flexidb init and npx flexidb codegen to get started in seconds
  • CLI tool — run prisma generate, migrate, studio, seed, diff, watch, and more across all schemas
  • Dynamic registrationdb.register() / db.unregister() for multi-tenant, runtime databases
  • Connection retry — automatic reconnection with exponential backoff
  • Event hooksonConnect, onDisconnect, onError, onRetry lifecycle callbacks
  • Enhanced statisticsdb.stats() with query counts, failure rates, and response times
  • Test utilitiescreateMockFlexiDB from flexidb/testing for unit testing
  • Graceful shutdown — auto-disconnects on SIGINT / SIGTERM
  • Type-safe — full TypeScript support with .d.ts declarations

Requirements

  • Node.js 18+
  • TypeScript 5+ (optional but recommended)
  • Prisma v6 or v7+ installed in your project

Installation

# npm
npm install flexidb

# yarn
yarn add flexidb

# pnpm
pnpm add flexidb

Note: Prisma is a peer dependency. Make sure you have prisma and @prisma/client installed in your project:

npm install -D prisma@latest @prisma/client@latest

Quick Start

0. Scaffold your project (optional)

If you're starting from scratch, run:

npx flexidb init

This creates prisma/schemas/, an example schema, and .env.example — then tells you exactly what to do next.

1. Create your schema directory

FlexiDB expects your Prisma schemas at prisma/schemas/<name>.prisma.

your-project/
└── prisma/
    └── schemas/
        ├── user.prisma
        └── analytics.prisma

2. Define your schemas

// 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 provider is "prisma-client" and the url field 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 set url = env("DATABASE_URL") in the datasource block.

3. Generate Prisma clients

npx flexidb generate

This runs prisma generate for every schema in prisma/schemas/ and outputs clients to the paths defined in your output fields.

4. Auto-generate your db.ts

npx flexidb codegen

This 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.

5. Use it anywhere

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' } });

Connecting to Databases

How you pass the connection URL depends on your Prisma version.

Prisma v7+ (Adapter-based)

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/client

Then 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 }),
});

Prisma v6 and Below (URL-based)

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")
}

Adding to an Existing Project

If you already have a Prisma project, FlexiDB integrates without breaking your existing setup.

Step 1 — Install FlexiDB

npm install flexidb

Step 2 — Reorganize schemas (optional)

If 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.prisma

Update 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
}

Step 3 — Generate all clients

npx flexidb generate

Step 4 — Wrap your clients with FlexiDB

import { 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 }),
});

CLI Reference

The flexidb CLI runs Prisma commands across all schemas (or a specific one) in prisma/schemas/.

npx flexidb <command> [subcommand] [options]

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)

Setup Commands

init

Scaffolds a new FlexiDB project. Creates prisma/schemas/, an example schema, and .env.example.

npx flexidb init

codegen

Reads 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 file

Prisma Commands

generate

Generates Prisma clients for all schemas.

npx flexidb generate
npx flexidb generate --schema user

migrate dev

Creates a new migration and applies it. Interactive — prompts for migration name.

npx flexidb migrate dev
npx flexidb migrate dev --schema user

migrate deploy

Applies all pending migrations. Use in CI/CD and production.

npx flexidb migrate deploy

migrate reset

⚠️ Destructive. Drops and recreates the database, then re-applies all migrations.

npx flexidb migrate reset --schema user

migrate status

Shows migration status for each schema.

npx flexidb migrate status

diff

Wraps 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.prisma

studio

Opens 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 found

db push

Pushes the current schema state to the database without creating migration files. Useful for prototyping.

npx flexidb db push
npx flexidb db push --schema analytics

db pull

⚠️ Destructive. Pulls the current database schema and overwrites the .prisma file.

npx flexidb db pull --schema legacy

validate

Validates all schema files.

npx flexidb validate

format

Formats all schema files.

npx flexidb format

seed

Runs 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 user

Seed 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.

watch

Watches prisma/schemas/*.prisma for changes and automatically re-runs npx flexidb generate when a schema is modified.

npx flexidb watch

Press Ctrl+C to stop. Useful during active schema development.

help

Displays all available commands.

npx flexidb help

API Reference

createFlexiDB(clients, options?)

Creates 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.


db.get(name, mode?)

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.


db.batch(operations)

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.


db.use(fn)

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();
});

db.group(name, databases)

Creates a named group of databases for bulk operations.

db.group('tenants', ['tenant_acme', 'tenant_xyz']);
db.group('analytics', ['events', 'metrics']);

db.healthGroup(groupName)

Runs a health check only for databases in a named group.

const status = await db.healthGroup('tenants');
// → { tenant_acme: true, tenant_xyz: false }

db.list()

Returns an array of all registered database names (including dynamically registered ones).

db.list(); // ['user', 'orders', 'analytics']

db.health()

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 }

db.stats()

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, and avgResponseTimeMs are tracked for operations run through db.batch().


db.register(name, client)

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.


db.unregister(name)

Disconnects and removes a database client at runtime.

await db.unregister('tenant_acme');

db.disconnect()

Manually disconnects all database clients and stops all background timers (health check, reconnect).

await db.disconnect();

Note: FlexiDB automatically disconnects all clients on SIGINT and SIGTERM. You usually don't need to call this manually.


Read/Write Splitting

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.


Multi-Tenancy

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);
}

Testing

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(),
});

Project Structure

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

Environment Variables

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! })
);

TypeScript

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';

License

ISC — see LICENSE.

Built with ❤️ in Indonesia. Open source. Zero magic. Just TypeScript.

About

Manage multiple Prisma databases in one app. Define, generate, and switch clients at runtime. No restart. No schema conflicts.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors