Postgres over HTTP for Vercel, Workers, and Lambda: the Neon serverless driver now works on Layerbase
Short version: every Layerbase Postgres now answers the HTTP query protocol that @neondatabase/serverless already speaks, so you pass your normal Layerbase connection string to neon() and query from Cloudflare Workers, Vercel Functions, or AWS Lambda with no TCP socket, no VPC, and no RDS Proxy. There is nothing to enable and no second hostname: the driver derives its endpoint by swapping the first label of your host for api., which our wildcard DNS and certificate already covered. Tagged-template queries, sql.query, typed results, sql.transaction([...]), NeonDbError codes, and the Drizzle, Prisma, Kysely, and @vercel/postgres adapters all work unchanged. Two things do not: interactive transactions over the driver's WebSocket mode, which we have not implemented, and databases with client-certificate enforcement, which an HTTP request cannot satisfy by design.
Until this week, the honest line in our serverless Postgres comparison was "we do not have an HTTP driver for Postgres, and if edge-runtime Postgres is your requirement, Neon's driver is the reason to pick them." That line is gone, and the way it went is the interesting part: we did not write a driver. Every Layerbase Postgres now answers the HTTP protocol that Neon's driver already speaks.
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL) // your Layerbase connection string
const users = await sql`select * from users where id = ${id}`That is the whole integration. The connection string is the same one the dashboard shows you. There is no flag to turn on, no second hostname, no @layerbase/serverless package to learn.
Contents
- Why an HTTP driver matters
- Why we implemented Neon's protocol instead of our own
- What works
- What does not work, and why
- How it is built
- Lambda without RDS Proxy
- FAQ
- Try it
Why an HTTP driver matters
Postgres speaks a binary protocol over a long-lived TCP socket. Three kinds of runtime have a problem with that.
Runtimes that cannot open a socket at all. Cloudflare Workers and Vercel Edge Functions give you fetch and little else. A normal pg client does not load there, and a database without an HTTP face is simply unreachable from them.
Runtimes that can open a socket but should not hold one. AWS Lambda, Vercel Functions, Cloud Run. Each cold start is a new process, each process opens its own connection, and a traffic spike turns into hundreds of backends trying to connect at once. On RDS this is the problem RDS Proxy exists to solve, at an hourly price, inside a VPC you now have to configure your function into.
Runtimes behind a small set of shared NAT addresses. A fleet of functions egressing through a handful of IPs looks, to a per-IP connection-rate limit, like one extremely impatient client.
An HTTP query endpoint answers all three: every request is a fetch, there is no socket to hold between invocations, and the server side can pool on your behalf.
Why we implemented Neon's protocol instead of our own
We could have published @layerbase/serverless. We chose not to, for a reason that has nothing to do with engineering effort.
A driver is only useful once the ecosystem around it exists: the Drizzle adapter, the Prisma adapter, the Kysely dialect, the framework templates, the thousand Stack Overflow answers. @neondatabase/serverless already has all of that. Drizzle ships drizzle-orm/neon-http. Prisma ships @prisma/adapter-neon. Kysely has kysely-neon. @vercel/postgres is a thin wrapper over it. PlanetScale made the same call for their Postgres product earlier this year, and it was the right one: meet the driver people already have in package.json rather than ask them to install yours.
We already do this twice elsewhere. Redis and Valkey databases on Layerbase expose an Upstash-compatible REST endpoint so @upstash/redis and @vercel/kv work as-is; MySQL and MariaDB expose a PlanetScale-compatible endpoint so @planetscale/database works as-is. Postgres was the conspicuous hole.
There is also a detail of the Neon driver that made this unusually clean. The driver decides where to send its HTTP requests by taking the hostname from your connection string and replacing the first label with api.: ep-cool-name.us-east-2.aws.neon.tech becomes api.us-east-2.aws.neon.tech. So mydb-cool-darkness.cloud.layerbase.dev becomes api.cloud.layerbase.dev, which our wildcard DNS record and wildcard certificate already served. We added an endpoint at /sql on that host. No per-database DNS, no extra certificates, no new hostname in your connection string.
What works
Everything the HTTP mode of the driver does against Neon:
- Tagged-template queries with parameters. Values are sent as Postgres parameters, never interpolated.
sql.query(text, params)for dynamic SQL with$1placeholders.- Typed results. The proxy returns raw Postgres text and the driver parses it with the same
pg-typesit uses against Neon, soint4comes back as a number,timestamptzas aDate,jsonbas an object, and your customtypesconfig is honored. fullResultsandarrayModeoptions, per call or per client.sql.transaction([...])withisolationLevel,readOnly, anddeferrable. The batch runs as a singleBEGIN ... COMMITon one connection and rolls back if any statement fails.- Errors as
NeonDbErrorwithcode,detail,hint, andposition, soerror.code === '23505'keeps meaning a unique violation. - Drizzle
neon-http,@vercel/postgres, and the Prisma and Kysely Neon adapters, which all sit on the same calls. - Hibernated databases. A free database that has been idle for 15 minutes wakes on the first HTTP request, the same way it wakes on the first TCP connection.
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
const sql = neon(process.env.DATABASE_URL)
export const db = drizzle(sql)Under the hood each request runs through the database's PgBouncer in transaction mode, which is the same pooler behind the pooled TCP connection string. A thousand concurrent Lambda invocations become a thousand short HTTP requests against a pool of twenty backend connections, which is what a pooler is for.
What does not work, and why
Two things, and we would rather you read them here than discover them mid-deploy.
Interactive transactions over WebSocket. The Neon driver has a second mode, Pool and Client from the same package, that tunnels the real Postgres protocol over a WebSocket so you can BEGIN, run application logic, and COMMIT across multiple round trips. We have not implemented that proxy. If you need an interactive transaction, use sql.transaction([...]) when the statements are known up front, or a normal TCP driver (pg, postgres.js) over the pooled connection string from a runtime that can open a socket. Multi-round-trip transactions from a Worker are the one case where Neon still has something we do not.
Databases with client-certificate (mTLS) enforcement. On the Pro plan you can require a client certificate on every connection. An HTTP request cannot present one, so the HTTP endpoint refuses mTLS-enforced databases with a clear error rather than quietly becoming a password-only side door. That refusal is the feature working, not a gap; if you want both, use the @layerbase/deno-mtls path described in Postgres mTLS client certificates.
One smaller thing: the endpoint accepts request bodies up to 1 MiB. A single query larger than that should be a COPY over TCP anyway.
How it is built
For the curious, since the design is short enough to describe.
The proxy lives inside the cloud API as POST /sql, routed by path regardless of hostname, so both the derived api.cloud.layerbase.dev endpoint and a custom neonConfig.fetchEndpoint pointed at the database's own hostname land in the same handler. It reads the connection string from the Neon-Connection-String header, resolves the database by hostname, and compares the username and password in constant time. A wrong password produces the same 28P01 password authentication failed body Postgres itself would send, and it never dials the database. The same guards the rest of the platform applies, suspended accounts, databases locked by a plan change, accounts mid-migration, apply here with the same HTTP statuses.
A passing request opens one pg client to the database's pooler port on the box, runs the query in array mode with identity type parsers so the wire text reaches the driver unchanged, and closes it. Batches run BEGIN, the statements in order, and COMMIT on that one client, with ROLLBACK on the first error. Response bodies match the driver's expectations field for field: fields, rows, command, rowCount, rowAsArray, and { results: [...] } for a batch.
The test suite does not mock the protocol. It installs the real @neondatabase/serverless package and points its fetchFunction at the proxy, so typed row parsing, batch transactions, isolation headers, and NeonDbError codes are asserted against the driver Neon ships, not against our reading of it. We learned the hard way elsewhere that a green test against a hand-written client proves very little.
Lambda without RDS Proxy
The tutorial shape this replaces is familiar: put the Lambda in the VPC, add a NAT gateway so it can still reach the internet, stand up RDS Proxy so the connection count survives a spike, store the password in Secrets Manager, wire the IAM role. Each step is reasonable and the sum is an afternoon.
The equivalent here is one environment variable and four lines of code, from a function that stays outside any VPC:
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL)
export const handler = async (event) => {
const [order] = await sql`select * from orders where id = ${event.orderId}`
return { statusCode: 200, body: JSON.stringify(order) }
}The pooler is the connection broker. The connection string is the credential, and it is the same one every other client uses. There is nothing to keep warm.
FAQ
Do I need a different connection string or a Layerbase-specific package?
Neither. You install @neondatabase/serverless, the package you would install anyway, and hand neon() the same connection string the dashboard shows you for psql and pg. There is no flag to turn on, no second hostname, and no @layerbase/serverless to learn. The driver works out where to send its HTTP requests from the hostname already in your string.
Does this work with Drizzle, Prisma, and Kysely?
Yes, because they all sit on the same calls the driver already makes. Drizzle's drizzle-orm/neon-http, @prisma/adapter-neon, kysely-neon, and @vercel/postgres work as they are. That interoperability is the whole reason we implemented an existing protocol instead of publishing our own driver: a driver is only worth having once the adapters, templates, and Stack Overflow answers around it exist.
Can I run an interactive transaction from a Worker?
Not over HTTP. The driver's Pool and Client mode tunnels the real Postgres protocol over a WebSocket for multi-round-trip transactions, and we have not implemented that proxy. If your statements are known up front, sql.transaction([...]) runs them as a single BEGIN ... COMMIT on one connection and rolls back on the first error. If you genuinely need application logic between BEGIN and COMMIT, use a normal TCP driver over the pooled connection string from a runtime that can open a socket.
What stops a traffic spike from exhausting connections?
Every request goes through the database's PgBouncer in transaction mode, the same pooler behind the pooled TCP string. A thousand concurrent Lambda invocations become a thousand short HTTP requests against a pool of twenty backend connections. There is no socket held between invocations and nothing to keep warm, which is why the Lambda version of this needs no VPC, no NAT gateway, and no RDS Proxy.
Will an HTTP query wake a hibernated database?
Yes. A free database that has been idle for 15 minutes wakes on the first HTTP request exactly the way it wakes on the first TCP connection. The one case that does not work over HTTP is a database with client-certificate enforcement turned on: an HTTP request cannot present a certificate, so the endpoint refuses those databases with a clear error rather than quietly becoming a password-only side door.
Try it
Create a Postgres database on the free tier, copy the connection string, and paste it into neon(). The serverless and edge guide has the Drizzle and Worker snippets, and the Connect dialog in the dashboard now shows the driver snippet next to psql and pg.
If you are on Neon today and the driver was the reason you stayed, the migration guide copies the schema and data in one read-only pass, and your application code does not change.
Keep reading
- We put the Postgres write-ahead log on object storage. We did not put the database there.Databricks rebuilt Postgres so that object storage is the database. We shipped a much smaller thing: the write-ahead log leaves the box continuously, the live database stays on local disk. Here is the whole design, the two bugs that taught us the most, and an honest account of what this architecture does not buy.
- Point-in-time restore is generally available, including FerretDBContinuous WAL archiving and restore-to-any-timestamp are now switchable from the Backups tab of any always-on PostgreSQL or FerretDB database on the Pro plan. A restore builds a new database and never touches the source. FerretDB gets it because its documents live in Postgres, and we proved that over the MongoDB wire before writing this.
- Point-in-time restore for Postgres: we lose seconds now, not an hourHourly dumps mean a bad DELETE at :59 costs you most of an hour, unrecoverably. We built continuous WAL archiving and restore-to-any-timestamp for always-on Postgres on Layerbase, then proved it with an executed drill: the row written two seconds before the target came back, the row written four seconds after it did not.
- ClickHouse sells Postgres now: the other way to get both enginesClickHouse shipped a managed Postgres service so you can run transactions and analytics with one vendor. Layerbase already ran both, flat-priced, with Postgres free and ClickHouse on a $15/mo plan. Here is the honest comparison.