@bounda-dev/postgresql stores everything in PostgreSQL through
Postgres.js.
import { postgresql } from "@bounda-dev/postgresql";
postgresql({ url: process.env.DATABASE_URL! });postgresql({ host: "localhost", port: 5432, database: "shop", user: "shop", password: "…" });Options
Section titled “Options”| Option | Default | Meaning |
|---|---|---|
url |
A postgres:// connection URL |
|
host, port, database, user, password, ssl |
The same, as parts | |
schema |
public |
The schema that holds every Bounda table; created if missing |
tablePrefix |
bounda_ |
Put in front of every table Bounda creates |
maxConnections |
10 |
Size of the connection pool |
How it stores things
Section titled “How it stores things”positionin the events table is aBIGSERIAL. Every append takes a transaction-scoped advisory lock (pg_advisory_xact_lock) before writing, so positions are handed out in commit order and a reader of the global stream never sees a gap that a later commit would fill. The lock bounds write throughput to what one connection can commit (the ceiling, and one store per tenant withschema). The lock, like the notification channel and the read models’ locks below, belongs to the store’sschema, so stores in two schemas of one database never wait on each other.- The stream version is checked in the same transaction as the write; a stale version rolls back
with a
ConcurrencyErrorand the command is retried with fresh state. So are the people the events name: the rows of their keys and ofbounda_personsare read under a share lock, after the advisory lock thatforgetandrestrictalso take first, so a person forgotten or restricted beside the append is so before it or after it, never across. - The transaction ends with
pg_notifyon a channel named after the schema and the events table (public.bounda_eventsby default), carrying the last position written. PostgreSQL delivers it at commit. Every worker listens on that channel through a dedicated connection Postgres.js keeps open and re-establishes on its own, and runs a dispatcher pass as soon as it hears; between notifications it polls only everyruntime.dispatcher.idleInterval. See Tuning. - Handler claims are single
INSERT … ON CONFLICT DO UPDATE … RETURNINGstatements; due scheduled commands are taken withFOR UPDATE SKIP LOCKED. Any number of instances can run the worker role and each due command is claimed by one of them at a time. - Payloads and metadata are
jsonb; read-model booleans, dates and JSON fields useboolean,timestamp with time zoneandjsonb. Numbers aredouble precision. Strings aretext COLLATE "C", which compares them by code point as SQLite does, so a range, anorderByand hand-written SQL over the column find the rows in the same order on both. The collation also folds case in ASCII only, again as SQLite does: in hand-written SQL,lower(),upper()andILIKEleaveÁas it is, unless the expression names another collation (lower("name" COLLATE "und-x-icu"), on a server built with ICU).
Storage and read models opened from the same postgresql(...) share one pool, closed when the
app stops.
In tests
Section titled “In tests”The adapter’s own tests start postgres:17 with Testcontainers,
and the same works for an app: see
against a real database.