Default behavior
postgresPlugin() creates MockPostgresClient when no client is supplied. connectionString and other connection options are not used to instantiate a driver in the current implementation. It does not automatically use Bun.SQL. The following runnable example is explicitly a mock check:
import { Volt, postgresPlugin, type PostgresClient } from 'voltts';
const app = new Volt();
// Without an injected client this is a mock, not a database connection.
app.plugin(postgresPlugin());
app.get('/database-check', async (c) => {
const db: PostgresClient = c.store.db;
const result = await db.query('SELECT 1');
return c.json({ rows: result.rows, mode: 'mock' });
});
if (globalThis._importMeta_.main) app.listen(3000);
export default app;Verify the mock
bun run index.ts
curl http://localhost:3000/database-checkSELECT 1 returns a mock row. Other statements generally return empty rows and do not persist data.
Real client contract
Connect a driver before plugin registration and pass it as postgresPlugin({ client }). The PostgresClient interface requires query(sql, params) returning { rows, rowCount }, transaction(callback), and optionally end(). The plugin decorates c.store.db and c.store.postgres. Use parameterized query text and arguments, not user-generated SQL strings. It calls end() during app.stop(). For a complete persistent setup using generated models, follow Prisma ORM.
Complete real PostgreSQL adapter
Install node-postgres and configure a reachable PostgreSQL database:
bun add pg
bun add --dev @types/pg
export DATABASE_URL='postgresql://postgres:postgres@localhost:5432/volt'import { Volt, postgresPlugin, type PostgresClient } from 'voltts';
import { Pool, type PoolClient } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await pool.query('SELECT 1');
function adapter(connection?: PoolClient): PostgresClient {
return {
async query<T>(sql: string, params = []) {
const result = await (connection ?? pool).query(sql, params);
return { rows: result.rows as T[], rowCount: result.rowCount ?? 0 };
},
async transaction<T>(callback: (db: PostgresClient) => Promise<T>) {
if (connection) throw new Error('Nested transactions need savepoint support');
const transaction = await pool.connect();
try {
await transaction.query('BEGIN');
const result = await callback(adapter(transaction));
await transaction.query('COMMIT');
return result;
} catch (error) {
await transaction.query('ROLLBACK');
throw error;
} finally {
transaction.release();
}
},
end: connection ? undefined : () => pool.end(),
};
}
const db = adapter();
const app = new Volt();
app.plugin(postgresPlugin({ client: db }));
app.get('/database-time', async () => ({ rows: (await db.query('SELECT now() AS time')).rows }));
app.post('/transaction-check', async () => {
const result = await db.transaction((tx) => tx.query('SELECT $1::text AS message', ['Hello Volt']));
return { rows: result.rows };
});
process.once('SIGTERM', async () => { await app.stop(); process.exit(0); });
app.listen(3000);bun run index.ts
curl http://localhost:3000/database-time
curl -X POST http://localhost:3000/transaction-checkEach transaction borrows one pool connection and releases it in finally. Parameter values are passed separately from SQL text. This follows the node-postgres transaction guide. Nested transactions require savepoints and are rejected by this small adapter. Verify the recipe against your database before extending it.