This page lists the adapters that connect CollegeDB to SQL databases and key-value stores. It covers what each adapter takes, which SQL dialect it reports, whether it declares an atomic batch, and which adapters can be the source of a shard move. It is for developers choosing a backend or wiring an existing database client into CollegeDB.
A provider adapts one client to the SQLDatabase or KVStorage interface that the router uses. Native bindings and custom providers both work, as long as they match the exported interfaces. Getting Started shows a first setup with the common adapters.
Pass a Drizzle sql tag as the second argument to createPostgreSQLProvider, createMySQLProvider or createSQLiteProvider to run statements through Drizzle. Without the tag, the adapter takes the native client.
| Adapter | Takes | Dialect | Declares atomicBatch | Journal source |
|---|---|---|---|---|
createPostgreSQLProvider |
A pg pool, or a pg client with singleConnection or lease |
postgres |
Yes for a pool. A client needs singleConnection or lease. |
Yes |
createMySQLProvider |
A mysql2 pool, or a connection with singleConnection or lease. MariaDB uses this adapter. |
mysql |
Same rule as PostgreSQL | Yes, for MySQL and MariaDB |
createSQLiteProvider |
A SQLite handle, with singleConnection: true or lease |
sqlite |
Only with singleConnection: true or lease |
Yes |
toProvider on a D1 binding |
A D1 binding | sqlite |
Yes | Yes, with the production caveat below |
| Drizzle over Postgres or MySQL | A Drizzle client and its sql tag |
postgres or mysql |
Yes, when the client has a transaction | Yes |
Drizzle over bun:sqlite or better-sqlite3 |
A Drizzle client and its sql tag |
sqlite |
Yes | Yes over bun:sqlite. better-sqlite3 is not in the test suite. |
| Drizzle over D1 or libsql over HTTP | A Drizzle client and its sql tag |
sqlite |
No. Batches run one statement at a time. | No. A move from this shard is refused. |
createHyperdrivePostgresProvider |
A Hyperdrive binding and a pg client factory |
postgres |
Yes | Yes |
createHyperdriveMySQLProvider |
A Hyperdrive binding and a mysql2 client factory |
mysql |
Yes | Yes |
createDrizzleSQLProvider |
A Drizzle client, its sql tag and an optional dialect |
As passed | Depends on the client | Depends on the client |
createDrizzleSQLProvider is a compatibility helper. The three Drizzle-aware factories above call it internally. The dialect of a Drizzle client is set by the factory you call, not detected from the driver.
atomicBatch means the adapter runs a batch inside one transaction. Without it, the statements of a batch run one at a time. A shard move needs its source to declare atomicBatch and to report a dialect that the journal supports. The supported dialects are sqlite, postgres and mysql. Builder writes on PostgreSQL, MySQL and MariaDB also need atomicBatch when journalShard is set. The full rules are in Migration Protocol.
Pools are detected from their methods. A mysql2 pool has getConnection, and a pg pool has connect with totalCount and idleCount. A pool leases one connection per batch, so the batch does not wait on other statements. Set singleConnection: true only on a dedicated connection. A single shared connection with that flag holds other statements back while a batch is open, so one slow batch delays every request that uses the connection. Use lease for a handle that CollegeDB does not recognize.
import { createPostgreSQLProvider } from '@earth-app/collegedb';
import { Client } from 'pg';
const client = new Client({ connectionString: process.env.POSTGRES_URL });
await client.connect();
const shard = createPostgreSQLProvider(client, { singleConnection: true });
The pg client above is one dedicated connection, so singleConnection is set. A pool would not set it.
The example stores shard mappings in Redis and puts rows on one PostgreSQL pool. Connect Redis before createClient runs, since the client starts reading the mapping store in the background as soon as it is created.
import { createClient as createRedisClient } from 'redis';
import { Pool } from 'pg';
import { createClient, createPostgreSQLProvider, createRedisKVProvider, type CollegeDBConfig } from '@earth-app/collegedb';
const redis = createRedisClient({ url: process.env.REDIS_URL });
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });
const config: CollegeDBConfig = {
kv: createRedisKVProvider(redis),
shards: {
'pg-east': createPostgreSQLProvider(pool)
},
strategy: 'hash'
};
await redis.connect();
const client = createClient(config);
await client.run('user-1', 'INSERT INTO users (id, name) VALUES (?, ?)', ['user-1', 'Taylor']);
Each example below pairs one set of adapters with the same routing calls used above.
The shard is the D1 database behind the NuxtHub Drizzle db. Drizzle over D1 cannot be a shard move source, as the table above notes.
import { db } from '@nuxthub/db';
import { kv } from '@nuxthub/kv';
import { sql } from 'drizzle-orm';
import { createNuxtHubKVProvider, createSQLiteProvider, initialize, run, first } from '@earth-app/collegedb';
initialize({
kv: createNuxtHubKVProvider(kv),
shards: {
'db-primary': createSQLiteProvider(db, sql)
},
strategy: 'hash'
});
await run('draft:home', 'INSERT OR REPLACE INTO drafts (id, content) VALUES (?, ?)', ['draft:home', '# Home']);
const draft = await first<{ id: string; content: string }>('draft:home', 'SELECT id, content FROM drafts WHERE id = ?', ['draft:home']);
toProvider detects a raw D1 binding and wraps it. It returns null for a value it does not recognize, so check the result before adding it to shards. One-Call Setup makes the same call for each binding in env.
import { createClient, createWorkersKVProvider, toProvider } from '@earth-app/collegedb';
const dbEast = toProvider(env.DB_EAST);
if (!dbEast) throw new Error('DB_EAST is not a supported binding');
const client = createClient({
kv: createWorkersKVProvider(env.KV),
shards: { 'db-east': dbEast },
strategy: 'hash'
});
A shard map can mix vendors. Each shard gets the adapter for its own client. The example routes rows across a D1 database, a PostgreSQL server and a MySQL server.
import { sql } from 'drizzle-orm';
import { drizzle as drizzlePg } from 'drizzle-orm/postgres-js';
import { drizzle as drizzleMySQL } from 'drizzle-orm/mysql2';
import { drizzle as drizzleD1 } from 'drizzle-orm/d1';
import { kv } from '@nuxthub/kv';
import postgres from 'postgres';
import mysql from 'mysql2/promise';
import {
createMySQLProvider,
createNuxtHubKVProvider,
createPostgreSQLProvider,
createSQLiteProvider,
initialize,
run
} from '@earth-app/collegedb';
const pgClient = postgres(process.env.POSTGRES_URL!);
const mysqlPool = mysql.createPool(process.env.MYSQL_URL!);
function setup(env: { DB_CF: D1Database }) {
initialize({
kv: createNuxtHubKVProvider(kv),
shards: {
'db-cf': createSQLiteProvider(drizzleD1(env.DB_CF), sql),
'db-pg': createPostgreSQLProvider(drizzlePg(pgClient), sql),
'db-mysql': createMySQLProvider(drizzleMySQL(mysqlPool), sql)
},
strategy: 'hash'
});
}
export default defineEventHandler(async (event) => {
setup(event.context.cloudflare.env);
await run('tenant:acme:user:1', 'INSERT INTO users (id, name) VALUES (?, ?)', ['tenant:acme:user:1', 'Ada']);
});
Drizzle over D1 is not a journal source, so a shard move cannot start from db-cf. The PostgreSQL and MySQL shards can be move sources when their clients declare atomicBatch, as the Drizzle rows above describe.
CollegeDB does not replace a Drizzle schema or the NuxtHub migration workflow. Generate and apply migrations with the tools you already use, then route runtime reads and writes through CollegeDB:
npx nuxt db generate
npx nuxt db migrate
Each shard needs the tables in its schema. Run the migration against every shard, or create the tables with ensureSchema from the client. A complete setup that does not use Cloudflare is in examples/provider-sandbox.ts.
The mapping store and the application cache can share one NuxtHub KV namespace. CollegeDB stores mappings under the shard: and multikey: prefixes, and it hashes the primary key in each mapping key by default. Application cache keys in the same store do not collide with them.
import { kv } from '@nuxthub/kv';
import { first } from '@earth-app/collegedb';
const cacheKey = `nuxtpress:post:${slug}`;
const cached = await kv.get(cacheKey);
if (cached) return cached;
const post = await first(cacheKey, 'SELECT * FROM blog_posts WHERE slug = ? LIMIT 1', [slug]);
await kv.set(cacheKey, post, { ttl: 3600 });
The first argument to first is the routing key. This example routes by the cache key, so the same slug always reaches the same shard.
A mapping store is a KVStorage. Any mapping store works with any SQL adapter.
| Store | Adapter |
|---|---|
| Workers KV | createWorkersKVProvider(kv, options) |
| NuxtHub KV | createNuxtHubKVProvider(kv) |
| Redis | createRedisKVProvider(client) |
| Valkey | createValkeyKVProvider(client) |
| In-memory, for tests | createInMemoryKVProvider() |
isKVStorage checks whether a value already matches the KVStorage interface, so a custom store that matches it needs no adapter. initializeFromEnv wraps a raw Workers KV namespace for you. Elsewhere, pass the namespace through createWorkersKVProvider.
Shard mappings are close to write-once, so the Workers KV adapter asks for a one-hour edge cache by default (cacheTtl, 3600 seconds, in src/providers.ts). A reassigned mapping can be served from an edge cache for up to cacheTtl seconds after the write. Lower the value when reassignments must reach every edge sooner. Pass 0 to send no cacheTtl and use the platform's own default. The in-process mapping cache adds its own window, set by mappingCacheTtlMs (30000 milliseconds by default).
import { createWorkersKVProvider, initialize } from '@earth-app/collegedb';
initialize({
kv: createWorkersKVProvider(env.KV, { cacheTtl: 300 }),
shards: { 'db-east': env.DB_EAST }
});
The Hyperdrive adapters create one client per request and reuse it for every statement in that request. A client cached in module scope throws on Workers, so either give the provider a way to identify the current request, or dispose of it at the end of the request.
Scoped, for one configuration shared across requests:
import { Client } from 'pg';
import { createHyperdrivePostgresProvider } from '@earth-app/collegedb';
let currentRequest: Request | undefined;
const shard = createHyperdrivePostgresProvider(env.HYPERDRIVE, (connectionString) => new Client({ connectionString }), {
scope: () => currentRequest
});
export default {
async fetch(request: Request, env: Env) {
currentRequest = request; // a new request gets a new client
// ... handle the request
}
};
Explicit, when the provider is built per request:
import { Client } from 'pg';
import { createHyperdrivePostgresProvider } from '@earth-app/collegedb';
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const shard = createHyperdrivePostgresProvider(env.HYPERDRIVE, (cs) => new Client({ connectionString: cs }));
ctx.waitUntil(shard.dispose());
// ... handle the request
}
};
The Hyperdrive adapters take PostgreSQL and MySQL clients only. D1 needs no connection pooling from Workers, so use a D1 binding instead.