Overview#
Neon is serverless PostgreSQL, with storage separated from
compute. The SQL is PostgreSQL's, so NeonDialect extends PostgresDialect and
inherits all of it. What changes is how you reach the database: the direct endpoint
or the PgBouncer-pooled one, over TCP, WebSocket or HTTP.| Property | Value |
|---|
| Dialect name | neon |
| Extends | PostgresDialect |
| Driver | pg, or @neondatabase/serverless (optional, loaded lazily) |
| Default port | 5432 |
| Cloud SDK | @neondatabase/serverless (WebSocket and HTTP transports only) |
| Local runtime | none — hosted service only |
Quick start#
Install#
$ npm install ts-prorm-orm pg
$ pnpm add ts-prorm-orm pg
$ yarn add ts-prorm-orm pg
$ bun add ts-prorm-orm pg
pg is the same driver the postgres dialect uses. @neondatabase/serverless and pg-copy-streams is optional: only the features that need it load it.Connect#
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'neon',
host: 'ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech',
database: 'neondb',
username: 'alex',
password: process.env.NEON_PASSWORD,
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'neon',
url: 'neon://alex:secret@ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech:5432/neondb',
});
A URL is folded into host, port, database, username and password before the dialect is built; any of those passed alongside it wins.Define a model#
import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
meta: { type: DataTypes.JSONB() },
});
await db.sync(); // CREATE TABLE — see Generated SQL below
meta is JSONB, which the containment query below needs.Query#
import { Op } from 'ts-prorm-orm';
await User.create({ email: 'ada@example.com', name: 'Ada' });
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});
Generated SQL#
Each block is the statement the model call above it hands the dialect's query(), captured with the driver stubbed out. The model layer inlines the values.Create table#
db.sync() for the model in the Quick start:CREATE TABLE
Insert, update and delete#
await User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });
Upsert#
await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });
upsert() also reads the row by primary key before and after the write, to return the instance.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});
JSON#
Containment on the meta column:import { Op } from 'ts-prorm-orm';
await User.findAll({ where: { meta: { [Op.contains]: { plan: 'pro' } } } });
Transaction with a savepoint#
await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
// A nested transaction is a SAVEPOINT; its failure undoes only its own work.
await db
.transaction(async (sp) => {
await User.destroy({ where: { id: 1 }, transaction: sp });
throw new Error('keep the row');
}, { transaction: t })
.catch(() => {});
});
Skip locked rows (SKIP LOCKED)#
A worker that claims rows from a queue table should pass over rows another transaction holds rather than wait for them. lock with skipLocked inside a transaction:await db.transaction(async (t) => {
await User.findAll({ where: { name: 'Ada' }, lock: { level: 'UPDATE', skipLocked: true }, transaction: t });
});
Type mapping for the portable descriptors: STRING → VARCHAR(255), auto-increment INTEGER → SERIAL, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → JSON. The type comparison lines the major engines up side by side.Connection: direct and pooled endpoints#
| Endpoint | Hostname | Behind it |
|---|
| Direct | ep-cool-darkness-123456.us-east-2.aws.neon.tech | Postgres |
| Pooled | ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech | PgBouncer, pool_mode=transaction |
In transaction mode, a server connection is yours for one transaction only. The
dialect detects the pooled endpoint from -pooler in the host or connection string;
you can override this with pooled. On the pooled endpoint it refuses the
following (with pooledSessionFeatures: 'warn' it warns once per feature instead):SET / RESET of session variables. SET LOCAL, SET TRANSACTION and
SET CONSTRAINTS end with the transaction, so they pass.
LISTEN / NOTIFY, including pg_notify() and the dialect's
listen() / notify().
DECLARE ... WITH HOLD cursors.
SQL-level PREPARE / DEALLOCATE. Protocol-level prepared statements from the
driver are supported.
Temporary tables with ON COMMIT PRESERVE ROWS / DELETE ROWS.
Session-level advisory locks (pg_advisory_lock, pg_try_advisory_lock,
pg_advisory_unlock, ...). The _xact_ variants pass.
Run schema migrations on the direct endpoint. neonPooledHost() and
neonDirectHost() convert one hostname into the other.Transports#
tcp (default)#
This is pg over TCP, the same as any Postgres. Neon requires TLS, so unless you
pass ssl, the dialect sends verified TLS ({ rejectUnauthorized: true }).new Prorm({
dialect: 'neon',
host: 'ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech',
username: 'alex',
password: process.env.NEON_PASSWORD,
database: 'neondb',
});
websocket#
This uses the Pool from @neondatabase/serverless, which is compatible with
node-postgres and runs over a WebSocket. Use it in serverless and edge runtimes that
have no TCP. Interactive transactions work. On Node 21 and earlier, pass a WebSocket
implementation:import ws from 'ws';
const db = new Prorm({
dialect: 'neon',
dialectOptions: {
transport: 'websocket',
connectionString: process.env.DATABASE_URL,
webSocketConstructor: ws,
},
});
http#
This uses the neon() HTTP query function, which sends one HTTPS request per query.
It is the fastest option for single queries.const db = new Prorm({
dialect: 'neon',
dialectOptions: { transport: 'http', connectionString: process.env.DATABASE_URL },
});
Queries go through sql.query(text, params, { fullResults: true }), so results
have the usual shape.
There are no sessions. startTransaction(), listen() and streaming cursors are
refused. httpTransaction([...statements], { isolationLevel, readOnly, deferrable })
runs a batch in one non-interactive transaction (sql.transaction).
fetchOptions and authToken are passed through.
Requests and responses are limited to 64 MB.
The SDK is loaded only when a websocket or http dialect connects. If it is
missing, the error says npm install @neondatabase/serverless. The query() API it
uses needs version 1.0 or later.Without connectionString, the dialect builds one from host, username,
password and database, ending in ?sslmode=require.PostgresDialect.connect() always builds a pg Pool. For these two transports,
NeonDialect.connect() therefore builds the SDK pool (or an HTTP adapter with the
pg result shape) itself and sets the base's private pool and _isConnected
fields. A test asserts this.Branches#
A Neon branch is a copy-on-write clone of the database, with its own compute and
hostname. To use a branch, connect with that branch's connection string. Nothing in
the dialect is branch-specific.References#
What differs from Postgres#
NeonDialect extends PostgresDialect (postgres) and
overrides 10 of its methods (Object.getOwnPropertyNames(NeonDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| Connection and execution | buildConnectionString, checkPooled, connect, disconnect, httpTransaction, listen, notify, query, resolvePoolConfig, startTransaction |
Capabilities#
Computed from the code — capabilitiesOf() (src/transaction-support.ts),
whereSupportOf() (src/query-builders/expression-render.ts),
sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the
builders — by the scratch test described in the
capability matrix.| Capability | neon |
|---|
| Transactions atomic | yes |
| Savepoints | yes |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | RETURNING on INSERT |
| Upsert | INSERT … ON CONFLICT (…) DO UPDATE |
| JSON column | JSON (JSONB → JSONB) |
| Vector column | VECTOR(3) |
| Array column | INTEGER[] |
| WHERE builder | 62 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family postgres: triggers yes; procedures yes; functions multiStatement; materialized views native; sequences yes; enums named; RLS policy |
| Streaming | queryStream() present; LISTEN present; createExtension() present |
Testing#
tests/dialects/neon.test.ts exercises the dialect with a mocked driver, so nothing touches a server.See also#
postgres — the parent dialect this one extends Modified at 2026-10-04 00:40:16