Drizzle Edge PostgreSQL Proxy Client
A client library for connecting to PostgreSQL databases from edge environments (Cloudflare Workers, Vercel Edge Functions, Deno Deploy, etc.) via an HTTP proxy. This package is compatible with Drizzle ORM and designed to work in all environments that support the Fetch API.
- π Edge-Ready: Works in all edge environments with fetch API support
- π Drizzle ORM Compatible: Drop-in replacement for Drizzle's PostgreSQL client
- π Secure: Support for authentication via bearer token
- π¦ Lightweight: Small bundle size perfect for edge deployments
- π TypeScript: Full TypeScript support with proper type definitions
- π Transactions: Support for running multiple queries in a transaction
- π§ͺ Tested: Comprehensive test suite for reliability
- π Array Support: Full PostgreSQL array parsing with element type awareness
- π Type System: Comprehensive type system for PostgreSQL data types
- π Session Tracking: Consistent session ID tracking for persistent connections
- Installation
- Quick Start
- Usage
- API Reference
- Setting Up a PostgreSQL HTTP Proxy
- Examples
- Development
- License
- Troubleshooting
# Using npm
npm install drizzle-edge-pg-proxy-client drizzle-orm
# Using yarn
yarn add drizzle-edge-pg-proxy-client drizzle-orm
# Using pnpm
pnpm add drizzle-edge-pg-proxy-client drizzle-orm
# Using bun
bun add drizzle-edge-pg-proxy-client drizzle-ormimport { drizzle } from 'drizzle-edge-pg-proxy-client';
import { eq } from 'drizzle-orm';
import { users } from './schema';
// Create a Drizzle client
const db = drizzle({
proxyUrl: 'https://your-pg-proxy-url.com',
authToken: 'your-secret-token', // Optional
schema: { users }
});
// Use it like any other Drizzle client
export async function getUser(id: string) {
return db.select().from(users).where(eq(users.id, id));
}First define your schema using Drizzle's schema definition:
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age')
});Then create a Drizzle client and use it to query your database:
import { drizzle } from 'drizzle-edge-pg-proxy-client';
import { eq } from 'drizzle-orm';
import { users } from './schema';
const db = drizzle({
proxyUrl: 'http://localhost:7432', // Use http:// for local development
authToken: 'your-secret-token', // Optional
schema: { users }
});
// Select all users
const allUsers = await db.select().from(users);
// Select a specific user
const user = await db.select().from(users).where(eq(users.id, 1));
// Insert a new user
const newUser = await db.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
age: 30
}).returning();
// Update a user
await db.update(users)
.set({ name: 'Bob' })
.where(eq(users.id, 1));
// Delete a user
await db.delete(users).where(eq(users.id, 1));If you prefer to use raw SQL queries, you can use the createPgHttpClient function:
import { createPgHttpClient } from 'drizzle-edge-pg-proxy-client';
const client = createPgHttpClient({
proxyUrl: 'https://your-pg-proxy-url.com',
authToken: 'your-secret-token' // Optional
});
// Execute a query directly
const users = await client.execute(
'SELECT * FROM users WHERE email = $1',
['user@example.com']
);
console.log(users); // Array of user objectsThe client also supports SQL template literals:
import { createPgHttpClient } from 'drizzle-edge-pg-proxy-client';
const client = createPgHttpClient({
proxyUrl: 'https://your-pg-proxy-url.com'
});
const userId = 1;
const result = await client.sql`
SELECT * FROM users WHERE id = ${userId}
`.execute();
console.log(result); // Array of resultsYou can also run multiple queries in a transaction:
import { createPgHttpClient } from 'drizzle-edge-pg-proxy-client';
const client = createPgHttpClient({
proxyUrl: 'https://your-pg-proxy-url.com'
});
const results = await client.transaction([
{
text: 'INSERT INTO users (name, email) VALUES ($1, $2)',
values: ['Alice', 'alice@example.com']
},
{
text: 'UPDATE users SET name = $1 WHERE email = $2',
values: ['Bob', 'bob@example.com']
}
]);
// results[0] contains the results of the first query
// results[1] contains the results of the second queryfunction drizzle<TSchema extends Record<string, unknown>>(options: {
proxyUrl: string;
authToken?: string;
schema: TSchema;
fetch?: typeof globalThis.fetch;
arrayMode?: boolean;
fullResults?: boolean;
typeParser?: TypeParser | Record<number, (value: string) => any>;
sessionId?: string;
logger?: LoggerOptions; // Added in v0.4.0
}): PostgresJsDatabase<TSchema>Creates a Drizzle ORM client connected to your PostgreSQL database via an HTTP proxy.
Parameters:
options: Configuration objectproxyUrl: URL of the PostgreSQL HTTP proxy serverauthToken(optional): Authentication token for the proxy serverschema: Drizzle ORM schema definitionfetch(optional): Custom fetch implementation (uses global fetch by default)arrayMode(optional): When true returns results as arrays instead of objectsfullResults(optional): When true returns complete result objects with metadatatypeParser(optional): Custom type parser instance or type parser configurationsessionId(optional): Explicit session ID for persistent connections (auto-generated if not provided)
Returns: Drizzle ORM database client
function createPgHttpClient(options: ClientOptions): PgHttpClient
interface ClientOptions {
proxyUrl: string;
authToken?: string;
fetch?: typeof globalThis.fetch;
arrayMode?: boolean;
fullResults?: boolean;
typeParser?: TypeParser | Record<number, (value: string) => any>;
sessionId?: string;
logger?: LoggerOptions; // Added in v0.4.0
}
interface LoggerOptions {
level?: LogLevel; // Default: LogLevel.Warn
logFn?: (level: LogLevel, message: string, data?: any) => void; // Default: console.log/warn/error
}
enum LogLevel {
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
None = 5,
}Creates a raw PostgreSQL HTTP client.
Parameters:
proxyUrl: URL of the PostgreSQL HTTP proxy serverauthToken(optional): Authentication token for the proxy serverfetch(optional): Custom fetch implementation (uses global fetch by default)arrayMode(optional): When true returns results as arrays instead of objectsfullResults(optional): When true returns complete result objects with metadatatypeParser(optional): Custom type parser instance or type parser configurationsessionId(optional): Explicit session ID for persistent connections (auto-generated if not provided)
Returns: A client with the following methods:
execute(query: string, params?: unknown[]): Promise<PgQueryResult>: Execute a SQL query with parameterssql(strings: TemplateStringsArray, ...values: unknown[]): QueryPromise<PgQueryResult>: Create a SQL template literal querytransaction(queries: { text: string, values: unknown[] }[], options?): Promise<PgQueryResult[]>: Execute multiple queries in a transactionquery(query: string, params?: unknown[], options?): Promise<PgQueryResult>: Direct query execution with optionsunsafe(rawSql: string): UnsafeRawSql: Create unsafe raw SQL for trusted inputs // -typeParser: Access to the type parser instance (currently not exposed)
Added in v0.4.0
You can configure logging by passing a logger object in the ClientOptions.
import { createPgHttpClient, LogLevel } from 'drizzle-edge-pg-proxy-client';
// Example 1: Set minimum log level to Debug
const client1 = createPgHttpClient({
proxyUrl: '...',
logger: {
level: LogLevel.Debug,
}
});
// Example 2: Use a custom logging function (e.g., with a dedicated logging library)
const client2 = createPgHttpClient({
proxyUrl: '...',
logger: {
level: LogLevel.Info,
logFn: (level, message, data) => {
// myLogger.log(LogLevel[level], message, data);
console.log(`[CUSTOM LOGGER][${LogLevel[level]}] ${message}`, data ? { data } : '');
}
}
});
// Example 3: Disable logging entirely
const client3 = createPgHttpClient({
proxyUrl: '...',
logger: {
level: LogLevel.None,
}
});Log Levels:
LogLevel.Debug(1)LogLevel.Info(2)LogLevel.Warn(3) - DefaultLogLevel.Error(4)LogLevel.None(5) - Disables logging
class TypeParser {
constructor(customTypes?: Record<number, (value: string) => any>);
// Add or override a type parser
setTypeParser(typeId: number, parseFn: (value: string) => any): void;
// Get a parser function for a specific type
getTypeParser(typeId: number): (value: string) => any;
}Custom type parser for PostgreSQL data types.
Example: Using a custom type parser
import { createPgHttpClient, TypeParser, PgTypeId } from 'drizzle-edge-pg-proxy-client';
// Create a custom type parser
const customTypeParser = new TypeParser();
// Override the default date parser to use a custom format
customTypeParser.setTypeParser(PgTypeId.DATE, (dateStr) => {
return new Date(dateStr + 'T00:00:00Z');
});
// Add a parser for a custom type
customTypeParser.setTypeParser(1234, (value) => {
return JSON.parse(value); // Example: parse a custom JSON type
});
// Use the custom type parser with the client
const client = createPgHttpClient({
proxyUrl: 'https://your-pg-proxy-url.com',
typeParser: customTypeParser
});
// Or pass a type parser configuration directly
const client2 = createPgHttpClient({
proxyUrl: 'https://your-pg-proxy-url.com',
typeParser: {
[PgTypeId.DATE]: (dateStr) => new Date(dateStr + 'T00:00:00Z'),
[PgTypeId.JSON]: (jsonStr) => JSON.parse(jsonStr)
}
});PgTypeId
The PgTypeId enum provides constants for all standard PostgreSQL data type OIDs:
enum PgTypeId {
BOOL = 16,
BYTEA = 17,
INT8 = 20,
INT2 = 21,
INT4 = 23,
TEXT = 25,
JSON = 114,
JSONB = 3802,
FLOAT4 = 700,
FLOAT8 = 701,
DATE = 1082,
TIMESTAMP = 1114,
TIMESTAMPTZ = 1184,
// ... and many more
}This client requires a PostgreSQL HTTP proxy server. You can implement your own, use the provided Docker implementation, or adapt one of the example implementations to your needs.
Updated in v0.3.3: Enhanced Auth.js (NextAuth.js) support with session ID tracking, improved transaction handling, and better error detection for null constraint violations. See the Docker README for more details.
A basic proxy implementation requires:
-
An endpoint that accepts POST requests to
/querywith a JSON body containing:{ "sql": "SELECT * FROM users WHERE id = $1", "params": [1], "method": "all" } -
An endpoint that accepts POST requests to
/transactionwith a JSON body containing:{ "queries": [ { "sql": "INSERT INTO users (name) VALUES ($1)", "params": ["Alice"], "method": "all" }, { "sql": "UPDATE users SET name = $1 WHERE id = $2", "params": ["Bob", 1], "method": "all" } ] } -
Authentication via bearer token (optional but recommended)
-
Support for X-Session-ID header for persistent connections (added in v0.3.3)
We provide a high-performance PostgreSQL HTTP proxy implementation using Docker and Docker Compose. This is the easiest way to get started.
# Clone the repository
git clone https://github.com/samuellembke/drizzle-edge-pg-proxy-client.git
cd drizzle-edge-pg-proxy-client
# Configure your database connection in .env file
# Start the proxy (connects to your external PostgreSQL database)
docker-compose up -dNow you can connect to your proxy at http://localhost:7432 and start using it with the client.
For more information see the Docker README.
This repository is configured for easy deployment with platforms like Coolify. The necessary Docker configuration files are located in the repository root:
docker-compose.yml- Docker Compose configurationDockerfile- Docker build instructions.env.coolify- Example environment variables for Coolify
- Connect your repository to Coolify
- Set up the required environment variables:
DATABASE_URL- Connection string to your PostgreSQL databaseAUTH_TOKEN- (Optional) Secret token for proxy authenticationHTTP_PORT- (Optional) External port for the proxy (default: 7432)CONTAINER_PORT- (Optional) Internal container port (default: 8080)
- Deploy the application
Important: You must provide your own PostgreSQL database. The proxy service will connect to your existing database using the DATABASE_URL environment variable.
The recommended proxy implementation uses Docker and Fastify for high performance and ease of setup:
- Fastify (Docker): High-performance proxy using Fastify and pg-native.
(Previous examples for Node.js/Express and Cloudflare Workers have been removed as the Docker implementation is the primary supported method.)
Check out the examples directory for more usage examples.
# Clone the repository
git clone https://github.com/samuellembke/drizzle-edge-pg-proxy-client.git
cd drizzle-edge-pg-proxy-client
# Install dependencies
bun install
# Run in development mode with watch
bun run dev
# Build the package
bun run build
# Run tests
bun test
# Run linting
bun run lintMIT
- Refactor: Split client code (
pg-http-client.ts) into multiple modules (types.ts,errors.ts,parsing.ts,utils.ts,query-promise.ts,index.ts) for better organization and maintainability. - Feature: Added configurable logging to the client (
createPgHttpClient). Users can now set log levels (Debug,Info,Warn,Error,None) and provide a custom logging function via theloggeroption. - Fix: Corrected payload field name inconsistency (
sqlvsquery) between client and proxy handlers to align more closely with Neon's protocol.
Complete rewrite of server implementation with enhanced session tracking and modularization:
- β Session ID tracking: Added X-Session-ID header for reliable client identification
- β UUID generation: Automatic UUID generation for session IDs (just like Neon)
- β Modular server architecture: Split monolithic server into logical modules for better maintainability
- β Enhanced DEFAULT handling: Improved detection and substitution of DEFAULT keywords
- β Improved transaction support: Better context tracking across transaction steps
- β Optimized Auth.js compatibility: Specifically targeting Auth.js account linking patterns
- β Improved error handling: Better error reporting with session context
This release provides complete compatibility with Neon's adapter for Auth.js integration. The session ID tracking ensures consistent client identification between requests, critical for Auth.js account linking operations.
Important: This version requires the updated PostgreSQL HTTP proxy (included in the Docker setup) that supports the X-Session-ID header.
Enhanced session context for improved Auth.js compatibility.
Initial session context implementation.
(Older versions omitted for brevity)
If you encounter an error like ERR_SSL_WRONG_VERSION_NUMBER when trying to connect to your PostgreSQL proxy, it usually indicates one of these issues:
-
Incorrect Protocol: You're using
https://when you should be usinghttp://(or vice versa). In local development usehttp://.// Correct for local development const db = drizzle({ proxyUrl: 'http://localhost:7432', // ... });
-
Proxy Not Running: Your PostgreSQL proxy server isn't running or isn't accessible at the specified URL.
-
Middleware/Edge Runtime Restrictions: When using in Next.js middleware or Edge Runtime, there might be additional restrictions on network requests. Make sure your proxy is accessible from these environments.
(Note: As of v0.4.0, specific Auth.js patterns involving DEFAULT on foreign keys might not work out-of-the-box due to standard PostgreSQL behavior. See below.)
If you encounter errors with the Auth.js DrizzleAdapter like "Unsupported database type", make sure:
- You're using the correct version of
@auth/drizzle-adapterthat's compatible with your Auth.js version - The adapter has correct table configurations
- The database client is correctly initialized before the adapter
If you encounter foreign key constraint errors such as null value in column "user_id" of relation "account" violates not-null constraint, this typically indicates that the SQL pattern being used relies on non-standard database behavior. Standard PostgreSQL interprets DEFAULT on a foreign key column (without a database-level default) as NULL, leading to this error. Auth.js's pattern sometimes uses DEFAULT expecting it to link to a previously inserted user ID within the same transaction.
This client and the provided proxy execute SQL according to standard PostgreSQL behavior. If you encounter this error:
- Verify the SQL: Check the exact SQL queries being generated by Auth.js/Drizzle.
- Consider Adapter/Library Update: Check if newer versions of
@auth/drizzle-adapteror Auth.js generate standard-compliant SQL (explicitly using the returneduser_idinstead ofDEFAULT). - Schema Check: Ensure your schema foreign key constraints are correctly defined:
// Example of proper Auth.js schema with Drizzle
import { relations } from "drizzle-orm";
import { pgTable, text, primaryKey, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("user", {
id: text("id").primaryKey(),
name: text("name"),
email: text("email").notNull().unique(),
// other user fields
});
export const accounts = pgTable("account", {
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
// other account fields
},
(table) => {
return {
pk: primaryKey({ columns: [table.provider, table.providerAccountId] })
};
});
// Define relations
export const usersRelations = relations(users, ({ many }) => ({
accounts: many(accounts)
}));
export const accountsRelations = relations(accounts, ({ one }) => ({
user: one(users, {
fields: [accounts.userId],
references: [users.id]
})
}));- Check Proxy Logs: Check the logs of your PostgreSQL HTTP proxy for any errors
- Verify Environment Variables: Make sure all required environment variables are set correctly
- Network Access: In production, ensure your Edge functions have network access to your proxy server
- CORS Issues: If you're getting CORS errors, make sure your proxy server has appropriate CORS headers configured
For more detailed debugging, try:
// Add this to see more details about connection issues
console.error = (...args) => {
console.log('Error:', ...args);
};
// Then initialize your client
const db = drizzle({
proxyUrl: 'http://localhost:7432',
// ...
});