Skip to content

Working With Multiple Connections

Fedaco supports more than one database connection per app. You'll want this when:

  • A single service talks to two different databases (e.g. a transactional Postgres plus a reporting MySQL).
  • You're sharding by tenant.
  • You're splitting reads from writes against a primary/replica pair.

This guide covers all three patterns. It assumes you've already read the Getting Started guide.

1. Register Multiple Named Connections

DatabaseConfig.addConnection(config, name?) takes an optional name. The first call without a name defaults to 'default' — the connection used by db(), schema(), and any Model without an explicit connection.

ts
import { DatabaseConfig } from '@gradii/fedaco';
import { mysqlDriver } from '@gradii/fedaco-mysql-driver';
import { postgresDriver } from '@gradii/fedaco-postgres-driver';

const db = new DatabaseConfig();

// Primary OLTP database — used by db() / schema() / Model when no
// connection is specified.
db.addConnection({
  driver: 'mysql',
  factory: mysqlDriver(),
  host: 'oltp.internal',
  database: 'app',
  username: 'app',
  password: 'secret',
});

// Secondary reporting database.
db.addConnection(
  {
    driver: 'pgsql',
    factory: postgresDriver(),
    host: 'reporting.internal',
    database: 'reports',
    username: 'reader',
    password: 'reader',
  },
  'reporting',
);

db.bootFedaco();
db.setAsGlobal();

2. Pick a Connection per Query

The query builder, schema builder, and model APIs all let you target a named connection.

Query builder

ts
import { db } from '@gradii/fedaco';

await db('reporting').query().from('daily_revenue').get();

Schema builder

ts
import { schema } from '@gradii/fedaco';

await schema('reporting').create('daily_revenue', (t) => {
  t.integer('day');
  t.integer('cents');
});

Models

Bind a model to a connection by setting connection on the @Table annotation:

ts
import { Column, Model, PrimaryGeneratedColumn, Table } from '@gradii/fedaco';

@Table({ tableName: 'daily_revenue', connection: 'reporting' })
export class DailyRevenue extends Model {
  @PrimaryGeneratedColumn() declare id: number;
  @Column() declare day: number;
  @Column() declare cents: number;
}

For one-off overrides at call time, use the useConnection helper:

ts
import { Model } from '@gradii/fedaco';
import { useConnection } from '@gradii/fedaco';

const rows = await User.useConnection('reporting').get();

useConnection is a tiny wrapper that calls Model.SetConnection() on a fresh instance and returns its query builder — see the model functions reference.

3. Read / Write Split

Fedaco ships first-class support for primary/replica setups. Define read and write blocks on the same connection config:

ts
db.addConnection({
  driver: 'mysql',
  factory: mysqlDriver(),
  database: 'app',
  username: 'app',
  password: 'secret',
  read: {
    host: ['replica-a.internal', 'replica-b.internal'], // randomly picked per resolve
  },
  write: {
    host: 'primary.internal',
  },
});

Behaviour:

  • select(...) queries go to the read PDO.
  • insert / update / delete / statement / inside a transaction go to the write PDO.
  • Once the connection has performed any write, it sticks to the write PDO for the rest of the request when sticky: true is set.
  • useWriteConnection() on a query forces the write side for that query.
ts
// Force a read query to use the primary (e.g. read-after-write checks):
await db().query().from('orders').useWriteConnection().get();

When host is an array, the driver picks one entry at random per fresh connection — that's how the connector spreads load across replicas.

4. Mixing Pools and Connections

Each named connection can have its own pool config — they're fully independent:

ts
db.addConnection({
  driver: 'mysql',
  factory: mysqlDriver(),
  database: 'app',
  pool: { max: 20 },
});

db.addConnection(
  {
    driver: 'pgsql',
    factory: postgresDriver(),
    database: 'reports',
    pool: { max: 5 },
  },
  'reporting',
);

Calling await db.getDatabaseManager().disconnect() tears down every cached connection (and its pool, if any) in parallel. See Connection Pooling for the full pool lifecycle.

5. Cross-Connection Caveats

A few things to keep in mind:

  • Transactions are per-connection. A single db().transaction(async tx => ...) only spans the connection it was started on. There's no two-phase commit across connections.
  • Foreign keys and joins can't cross connections (or even databases on most drivers without explicit setup). Plan your schema so each connection owns a coherent slice of data.
  • Connection name vs database name. A connection is what you named via addConnection(config, name). A database is what's in config.database. Two connections can point at the same physical database with different credentials, pool sizes, or read/write topology.

Further Reading

Released under the MIT License.