Volt v0.1 · TypeScript APIs on Bun · Get started
Docs/Plugins Ecosystem/PostgreSQL Integration

PostgreSQL Integration

Understand the client contract and avoid accidentally using the mock.

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:

index.ts
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

bash
bun run index.ts
curl http://localhost:3000/database-check

SELECT 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:

bash
bun add pg
bun add --dev @types/pg
export DATABASE_URL='postgresql://postgres:postgres@localhost:5432/volt'
index.ts
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);
bash
bun run index.ts
curl http://localhost:3000/database-time
curl -X POST http://localhost:3000/transaction-check

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