Use AWS Aurora DSQL with DaloyJS
Aurora DSQL is AWS's serverless, distributed PostgreSQL-compatible service. It speaks the Postgres wire protocol, so you use the standard pg driver, but the password is a short-lived IAM auth token instead of a static secret. Pair it with the Lambda adapter for an AWS-native stack.
1. Provision a cluster
Create a cluster in the AWS console or via the CLI. Note the endpoint hostname (<cluster-id>.dsql.<region>.on.aws) and the cluster ARN.
2. Create a least-privilege database role
DSQL has a built-in admin role. Use it to set up the schema and run migrations, not for application traffic. Connect once as admin (for example from CloudShell with psql) and create a login role for the app, mapped to the IAM role your Lambda function or server runs as:
Create your tables in the app schema. ON ALL TABLES covers only tables that exist when it runs, so repeat the GRANT in the migration that adds a table. List the IAM mappings with SELECT * FROM sys.iam_pg_role_mappings; and remove one with AWS IAM REVOKE.
Then give the runtime IAM role only dsql:DbConnect on this cluster. dsql:DbConnectAdmin (which signs admin tokens) belongs to the migration pipeline only.
3. Install
4. How the token works
- 01DsqlSignernew DsqlSigner({ hostname, region })
- 02Sign a tokensigner.getDbConnectAuthToken() · role app
- 03Open a connectiontoken checked once, at connect
- 04Reuse itvalid until the 60-minute connection limit
- 05Pool recyclesmaxLifetimeSeconds < 3600 · new token per connection
Signing is a local operation; it does not call AWS. The token (15 minutes by default) is checked only when a connection opens, and an established connection stays valid after it expires. The limits that matter are server-side: DSQL closes every connection after 60 minutes and aborts any transaction older than 5 minutes. So you do not need a token per request; you need a fresh token for every new connection, and a pool that retires connections before the 60-minute mark.
5. A module-scope pool
Keep the declare module block in the module that creates the pool, not in a separate .d.ts file. Declaration files are exempt from type-checking when skipLibCheck is on (the scaffolded default), so a broken import inside a .d.ts fails silently and state.db degrades to any.
Or use the AWS connector
AWS recommends its Aurora DSQL Connector for node-postgres (@aws/aurora-dsql-node-postgres-connector, pre-1.0 on npm). It wraps pg Client and Pool, generates a token per connection, reads the region from the hostname, and has a transaction() helper that retries optimistic-concurrency conflicts:
6. Wire it into the app
On Node, Bun, or Deno, decorate the pool and drain it on shutdown:
On the Lambda adapter, use the same module-scope pool: warm invocations reuse its connections, password() signs a new token whenever the pool opens one, and maxLifetimeSeconds keeps every connection under the 60-minute limit. Lambda has no shutdown hook, so onClose() never runs there; the small max and the idle timeout keep a frozen container from holding many connections.
7. Retry optimistic concurrency conflicts
DSQL uses optimistic concurrency control: transactions never wait on locks, and a conflicting transaction fails at COMMIT with SQLSTATE 40001 (the AWS connector also retries OC000 and OC001). Keep transactions short and idempotent, and retry them:
Let unexpected errors throw: DaloyJS redacts 5xx detail in production, so the driver error is not echoed to clients.
8. Limits that affect ORM migrations
DSQL is PostgreSQL-compatible, not identical. These rules (from the AWS migration guide and quotas) break most migration tools' defaults:
- A transaction can hold only one DDL statement, and DDL and DML need separate transactions. Run each migration statement on its own, and turn off any "wrap the whole migration in one transaction" behavior.
- Use
CREATE INDEX ASYNCinstead ofCREATE INDEX, andDELETE FROMinstead ofTRUNCATE. - No temporary tables, triggers, or procedural languages such as PL/pgSQL (SQL functions are supported).
- A write transaction can modify at most 3,000 rows and 10 MiB, and lives at most 5 minutes, so batch backfills.
- The isolation level is fixed at Repeatable Read, there is one database (
postgres) per cluster, and up to 10 schemas. - Prefer UUID primary keys over sequential ids to spread writes; see sequences and identity columns if you need them.
Run migrations from CI with an admin token (signer.getDbConnectAdminAuthToken(), which needs dsql:DbConnectAdmin), then re-grant the app role on any new tables.
With Drizzle ORM
With Prisma
Use the pg driver adapter and pass it the token-refreshing pool: new PrismaPg(pool) from @prisma/adapter-pg, then new PrismaClient({ adapter }) from your generated client. See Prisma. Check the limits above before running prisma migrate against DSQL.
Things to remember
- Cloudflare Workers are not covered by this guide. Hyperdrive stores static credentials, and DSQL only accepts short-lived IAM tokens. On Workers, use Neon, PlanetScale, or another Postgres host through Hyperdrive.
- Tokens only gate new connections; the 60-minute connection limit and 5-minute transaction limit are what your pool and transactions must respect.
- Check the known issues before relying on niche extensions or features.
See also the database hosting overview or jump back to Prisma / Drizzle.