Using Your Database in Code
Learn about Using Your Database in Code in WaymakerOS.
Your app's code reads and writes its Postgres database at runtime with a single SQL client. There are two entry points — same sql / query surface, so patterns transfer 1:1 between them:
- Apps (Next.js server code, API routes, server actions) —
import { sql } from '@waymakeros/db' - Ambassadors and Agents —
ctx.postgres, alongsidectx.tablesandctx.ai
Either way it's full read and write (unlike the read-only SQL console), it's standard Postgres so standard SQL is all you need, and the hosting engine stays hidden — your code never imports or names a database vendor, so it stays portable with no lock-in.
Both entry points work because connecting the database injects DATABASE_URL into your app's environment. You never touch that value — the client reads it for you.
Install it
In an App, add the client once:
npm install @waymakeros/db
Ambassadors and Agents need no install — ctx.postgres is already there.
Two ways to query
The examples below use sql / query directly (an App with @waymakeros/db). In an Ambassador or Agent, the identical calls are ctx.postgres.sql / ctx.postgres.query.
Tagged template — the safe default. Values are inserted as parameters automatically, so there's no way to accidentally build an unsafe query:
import { sql } from '@waymakeros/db'
const id = 42
const rows = await sql`
SELECT * FROM customers WHERE id = ${id}
`
Text and parameters — the conventional form, when you'd rather build the query string yourself:
import { query } from '@waymakeros/db'
const rows = await query(
'SELECT * FROM customers WHERE id = $1',
[42]
)
Both return an array of row objects (Promise<any[]>). Reads return the matching rows; writes return whatever your SQL's RETURNING clause asks for.
Reading and writing
import { sql } from '@waymakeros/db'
// Read
const open = await sql`
SELECT id, email FROM orders WHERE status = ${'open'}
`
// Write — insert and get the new row back
const [order] = await sql`
INSERT INTO orders (customer_id, total)
VALUES (${customerId}, ${total})
RETURNING *
`
// Update
await sql`
UPDATE orders SET status = ${'shipped'} WHERE id = ${order.id}
`
Querying as the signed-in user
sql and query connect as the database owner, which is exempt from access rules — so
rules you have written do not restrict them. That is the right connection for migrations,
admin tasks and background jobs.
For anything an end user can reach, use forUser(token) instead. It connects as the signed-in
user, so the database enforces your access rules:
import { forUser } from '@waymakeros/db'
const notes = await forUser(token).sql`SELECT * FROM notes`
// → only the rows your access rules allow this user to see
Full detail: Access Rules.
Where it's available
| Runtime | How you reach the database |
|---|---|
| App (server code / API routes / server actions) | import { sql } from '@waymakeros/db' |
| Ambassador | ctx.postgres — alongside ctx.tables, ctx.ai |
| Agent | ctx.postgres — alongside the agent's memory and schedules |
The query surface is identical across all three, so code moves between them unchanged apart from the import vs. ctx.postgres prefix.
Two mistakes that cost a day each
Both come from writing the data layer the way you would on a normal Node server. Neither is obvious, and neither fails in a way that points at the cause.
Don't use pg. pg (node-postgres) is the default Postgres client in the Node world, so it
is the first thing most people install — but it opens a raw TCP socket, which this runtime cannot
do. The build fails with Could not resolve "pg-cloudflare", naming a package you have never
heard of, hundreds of lines into a log. Use @waymakeros/db; it is HTTP-based and works here.
Don't hold a connection pool across requests. Importing sql / query at the top of a file
and reusing them is safe — that client holds no socket. A pool is different: cached at
module scope, it makes requests hang unpredictably once deployed. One page returns 200, the next
500, the next 200 — same code, same deploy — which reads as a bug in whichever page failed. If
you genuinely need an interactive transaction, open a pool inside that one function and close it
before returning.
Things worth knowing
- Always parameterise. Use
${value}in the tagged template, or$1/$2with the params array — never paste user input straight into the query string. Both forms above do this for you. - Create tables first. The client reads and writes data; it doesn't set up your schema. Tables come from migrations. If no database is connected, it throws a clear setup error.
- In-code access vs the SQL console. Your app's runtime connection is full read/write. The console and the AI query tools are read-only on purpose — they're for you to inspect data safely, not for structural change.
Next Steps
- Changing Your Schema — create the tables you'll query
- The Data API — reach your data over HTTP instead of in code
- What is an Agent? — stateful workers that use
ctx.postgres