{"protocol":"clank-doc/1","frameworkVersion":"0.24.0","slug":"postgres-application","title":"PostgreSQL application databases","description":"openPostgres provides a supported subset of Clank's generated table and function contract. It uses a dedicated Node worker and PostgreSQL's parameterized v3 protocol with no NPM driver.","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/postgres-application","source":"docs/postgres-application.md","headings":["Supported contract","Authentication and transport","Reconnect and uncertain commits","Resource limits and verification"],"tableOfContents":[{"id":"supported-contract","title":"Supported contract","level":2},{"id":"authentication-and-transport","title":"Authentication and transport","level":2},{"id":"reconnect-and-uncertain-commits","title":"Reconnect and uncertain commits","level":2},{"id":"resource-limits-and-verification","title":"Resource limits and verification","level":2}],"markdown":"# PostgreSQL application databases\n\n`openPostgres` provides a supported subset of Clank's generated table and function contract.\nIt uses a dedicated Node worker and PostgreSQL's parameterized v3 protocol with no NPM driver.\n\nSQLite remains the default application store and the native store for authentication, queues,\nsearch, reviewed actions and offline mutation receipts. PostgreSQL does not replace the\nplatform control catalog or translate SQLite SQL.\n\n```ts\nimport {defineDatabase, defineTable, defineBackend, openBackend, openPostgres, s}\n  from \"@clank.run/framework\";\n\nconst schema = defineDatabase({\n  notes: defineTable({title: s.string({max: 200}), complete: s.boolean()}),\n});\nconst database = await openPostgres(schema, {\n  host: \"database.example.test\",\n  database: \"application\",\n  user: \"application\",\n  password: process.env.APPLICATION_DATABASE_PASSWORD!,\n  namespace: \"notes_application\",\n  tls: {ca: process.env.APPLICATION_DATABASE_CA},\n});\nconst definition = defineBackend({schema}).functions(({publicQuery, publicMutation}) => ({\n  list: publicQuery({args: {}, handler: ({db}) => db.table(\"notes\").collect()}),\n  add: publicMutation({args: {title: s.string({max: 200})},\n    handler: ({db}, {title}) => db.table(\"notes\").insert({title, complete: false})}),\n}));\nconst backend = await openBackend(definition, {database});\n// Mount backend.handle using the ordinary generated HTTP/MCP transport.\n```\n\nThe caller supplies a dedicated database role and database. Give that role access only to\nthis application's database; the fixed `clank_pg_applications`, `clank_pg_documents` and\n`clank_pg_changes` tables are internal, rather than an authorization layer for arbitrary SQL\nclients. The initial setup requires table/index creation permission. A namespace separates\napplication rows inside those tables. Namespace and schema identity are validated on reopen;\nchanged schemas and unknown protocol versions fail before application writes. Automated schema\nmigrations, copying SQLite data and dual writes are unsupported.\n\n## Supported contract\n\nThe adapter implements synchronous `read`, `tracked` and `transaction` callbacks, `table`,\n`get`, `insert`, `patch`, `replace`, `delete`, scalar `where` comparisons, `orderBy`, `limit`,\n`collect`, `first`, expected document versions, change subscriptions and retained global\nrevisions. One native connection executes each interactive transaction. Writes lock the\napplication revision row and commit all tables and change metadata together; reads use a\nrepeatable-read snapshot with its own revision. Equal canonical JSON does not advance a\ndocument version. Returned documents are immutable. Nested callbacks, returned promises and\nexpired table/query handles are rejected.\n\nOwned tables work through trusted server callbacks and an explicit `DatabaseScope`:\n\n```ts\ndatabase.transaction(db => db.table(\"privateNotes\").insert({title: \"Owned note\"}),\n  {userId: trustedCurrentUserId});\n```\n\nDefine that table with `.owned()`. Filtering executes in native SQL, and writes cannot change\nthe stored owner. A null owner is anonymous and cannot access owned tables. Omitted scope is\ndeliberate trusted server access for reads and existing-row changes; owned inserts still need\nan explicit owner. Resolve identities on the server. Never accept a browser-provided owner as\nauthorization.\n\nGenerated `openBackend` integration initially accepts only public functions over the exact\nsame public-table schema. It refuses owned schemas, authenticated functions and SQLite-only\nauth/jobs/activity/review/offline-receipt/bucket integrations before opening native services.\n`BackendRuntime` infers the concrete storage type; ordinary SQLite runtimes retain their\nexisting native capability. PostgreSQL has no private SQLite handle.\n\n`history`, `restore`, `purgeDeleted`, aggregates, search and SQLite-native services are\nunsupported. The `unsupported` capability tuple names this boundary; these operations throw\n`PG_OPERATION_UNSUPPORTED`, rather than returning an empty or approximate result. Queries\nsupport declared scalar fields and metadata, at most twenty comparisons and one ordering.\nComplex field comparisons and undefined comparison values are rejected. Limits are positive;\nover-capacity `collect` fails instead of silently returning a partial collection.\n\n## Authentication and transport\n\nTLS verifies both the trusted CA and server identity by default. Supply a private CA with\n`tls.ca`; `tls.serverName` can explicitly bind a certificate identity. There is no\n`rejectUnauthorized` switch. Plaintext is available only with `tls: \"loopback\"` and the literal\n`127.0.0.1` or `::1`, for an explicitly isolated development server. DNS names cannot use that\nexception. The adapter requires SCRAM-SHA-256, bounded printable ASCII passwords and\n4096–200000 iterations. Unicode SASLprep, SCRAM-PLUS, trust, cleartext passwords, MD5 and other\nauthentication mechanisms are unsupported. Unsupported wire messages fail closed.\n\nErrors contain a fixed Clank error code and, for known native statement rejection, SQLSTATE.\nThey exclude PostgreSQL error text, credentials, cancellation keys, SQL parameters and\nconnection URLs. Keep credentials and CA material outside committed files and evidence exports.\nUse [PostgreSQL's SASL documentation](https://www.postgresql.org/docs/current/sasl-authentication.html)\nand [protocol reference](https://www.postgresql.org/docs/current/protocol-message-formats.html)\nwhen configuring a compatible server.\n\n## Reconnect and uncertain commits\n\nExternal changes publish selective table/document/owner invalidation. If retained revisions\nwere missed, reconnect or synchronization publishes conservative invalidation. Revisions never\nmove backward. `changePollIntervalMs` defaults to 100 ms; zero disables polling, while ordinary\nreads and the version getter still synchronize. Polling runs only when listeners exist.\n\nA connection failure or deadline around `COMMIT` throws `PG_COMMIT_UNKNOWN` and poisons the\nsession. The write may have committed. No write is automatically replayed. Call\n`await database.reconnect()` and inspect your application's native records and revision before\ndeciding what to do. Your application needs its own durable business identifier when an action\nmust distinguish an accepted operation from a new request. This subset does not provide\ntransactional mutation receipts. A known statement rejection preserves its SQLSTATE and rolls\nback; native transport failures require reconnect. Quiesce writers before rollback or engine\nreplacement, and retain PostgreSQL data until recovery is explicitly resolved.\n\n## Resource limits and verification\n\nDefaults are a five-second operation/transaction deadline, 1000 returned rows, 4 MiB wire/shared\nresponse, 100 mutation operations, 64 KiB canonical document and 10000 retained revisions.\nConfigurable ceilings are thirty seconds, 10000 rows, 16 MiB response, 1000 mutation operations\nand 100000 revisions. SQL templates and parameters are bounded. Native statement, lock and idle\ntransaction timeouts also apply. Change retention additionally caps each namespace at 100000\nrows, removing entire older revisions; missed changes cause conservative invalidation. There\nare at most 128 schema tables and 1000 change listeners. Synchronous calls block their caller\nuntil the bounded worker response, so use an application process sized for this execution model.\n\nChange polling reads revision, retention and records in one repeatable native snapshot, so concurrent\npruning cannot silently omit an already observed change.\n\nFor native verification from a source checkout, install PostgreSQL server binaries and OpenSSL on a disposable Linux\nhost, then run as an ordinary user:\n\n```sh\nnode scripts/verify-postgres.mjs\nnode scripts/verify-postgres.mjs --full\n```\n\nThe verifier creates a fresh private cluster with SCRAM and verified TLS, uses the same table\nand generated-function suite for SQLite and PostgreSQL, stops the exact owned cluster and\nremoves its private files. It never operates on an existing server. Tests exercise owner\nisolation, atomic rollback, conflicts, two sessions, actual writer SIGKILL, a real TCP drop after\nnative commit, reconnect without replay, retention gaps, schema mismatch, bounds and TLS/credential\nrefusal. No production PostgreSQL or provider certification is implied.\n"}