Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Data

Data

Stores

On this page

A store is somewhere your app keeps records: a PostgreSQL database, or a Redis server. Every entity lives in exactly one store, main unless its file says store: <name>. With no database declared, main is a PostgreSQL database and you don't declare it yourself. Anything your app never saves is a type, not an entity.

You declare other stores as connections in app.jadpo, in its connections: block (see Integrations for connections in general). Each store's driver is written in the source, so every environment, from your machine to production, runs the same engine. Here a shop keeps its orders in a database apart from its catalogue:

example
// app.jadpo in a shop that keeps its orders apart from its catalogueapplication {    connections: {        catalogue: {            driver: Driver.postgres        }        orders: {            driver: Driver.postgres        }    }}
example
/// Something the shop sells.entity Product {    id: Uuid    name: Text    store: catalogue}
example
/// A customer's order.entity Order {    id: Uuid    ordered_at: Instant {        generated: Generated.on_create    }    store: orders}

An action writes to at most one store. Work that spans two stores goes through an event and a subscriber (Jobs and events). An entity in Redis must say when its records expire (Entities in Redis).

No database unless something needs one. An app that uses no database has none: no URL to configure, no migrations, nothing for jadpo dev to start. main on PostgreSQL is implied only once something needs a database. These need one:

Where something needs a database and none can hold it, jadpo check reports an error naming the feature that needs one and the connection to add. That happens when the app declares databases but none called main, or names a Redis store for something only a database can keep.

The database's address. A database connection's address is a setting of the type its driver needs: url: config.database_url, with database_url: PostgresUrl in config.jadpo (Database and other connection URLs). jadpo migrate apply and jadpo migrate status read it from the environment, then .env.local, or take the URL with --url. A connection's other settings are described under Integrations.

Not built yet: the "only once something needs one" part, which comes with the first feature that can run without a database, and the D1 driver (Actions on D1).

PostgreSQL over TLS

A database connection always uses TLS. Jadpo checks the server's certificate against Mozilla's root certificates, which are compiled into Jadpo, and checks that it is for the host name in the address. The one exception is this machine: a Unix socket, localhost, 127.0.0.1 or ::1 is reached without TLS.

The connection, in app.jadpo where a review sees it, decides anything else. Its address never does. There are two options.

Your own certificate authority: authority:. Use it when your database's certificate is signed by a private authority rather than a public one, such as Amazon RDS's root for its region or Cloud SQL's server CA. authority: config.<name> names a Certificate setting, read from its PEM (Standard library). Only that authority is trusted, and the connection then uses TLS to this machine too, except over a Unix socket, which PostgreSQL never secures.

No TLS: tls: false. Use it for a server elsewhere, on a private network, that offers no TLS. The network report lists the connection as reached without TLS.

example
application {    connections: {        main: {            driver: Driver.postgres            url: config.database_url            authority: config.database_authority        }        legacy: {            driver: Driver.postgres            url: config.legacy_url            tls: false        }    }}
example
config {    database_authority: Certificate    database_url: PostgresUrl    legacy_url: PostgresUrl}

An address that sets TLS itself, with sslmode, sslrootcert or another of libpq's ssl… settings, is refused as the app starts, by jadpo config check and by jadpo migrate, with the error configuration.value_invalid (Configuration and time). A Certificate setting that isn't the PEM of exactly one certificate is refused the same way.

A server elsewhere that offers no TLS, or whose certificate the check refuses, is a fault naming the server. It is never retried as if the database were only down for now.

Each attempt at a server, from opening the socket to the server being ready, waits at most the address's connect_timeout. When the address sets none, or sets 0 (which libpq reads as "forever"), it waits 10 seconds. Then the next host is tried. A deployable (a program jadpo build makes, such as a server or a Worker) that is waiting for its database, or running a schedule, still stops when it is sent SIGTERM.

jadpo migrate, jadpo schedules, jadpo runs, jadpo deliveries and jadpo ai usage reach a database the way its connection says, and take --url too.

A database connection takes no setting other than url:, authority: and tls: (connection.key_invalid):

example
application {    connections: {        main: {            driver: Driver.postgres            url: config.database_url            sslmode: "require"        }    }}
text
error: A connection with `driver: Driver.postgres` takes no `sslmode:` --> app.jadpo:6:13  |6 |             sslmode: "require"  |             ^^^^^^^ not a database setting  reason: A connection to a PostgreSQL database gives `url:`, a `PostgresUrl` setting, and may give `authority:`, a `Certificate` setting its server's certificate is checked against, or `tls: false` for a server elsewhere that offers no TLS.  next: Remove `sslmode:` (agent)  rule: connection.key_invalid  help: https://jadpo.dev/docs/errors/connection.key_invalidcheck failed: 1 error, 2 files

authority: reads a Certificate setting (connection.value_invalid):

example
application {    connections: {        main: {            driver: Driver.postgres            url: config.database_url            authority: config.database_authority        }    }}
text
error: `main`'s `authority:` must be a `Certificate` setting --> app.jadpo:6:24  |6 |             authority: config.database_authority  |                        ^^^^^^^^^^^^^^^^^^^^^^^^^ `database_authority` is `Text`  reason: `authority:` is the authority its server's certificate is checked against, as PEM: a `Certificate` setting, `config.<name>`, read from each deployment's environment.  next: Declare `database_authority: Certificate` in `config.jadpo` (agent)  rule: connection.value_invalid  help: https://jadpo.dev/docs/errors/connection.value_invalidcheck failed: 1 error, 2 files

tls: is only ever written false, since TLS is already the default (connection.value_invalid):

example
application {    connections: {        main: {            driver: Driver.postgres            url: config.database_url            tls: true        }    }}
text
error: `main`'s `tls:` is written only as `false` --> app.jadpo:6:18  |6 |             tls: true  |                  ^^^^ the default already  reason: A database connection uses TLS unless it says `tls: false`, for a server elsewhere that offers none; it is decided in `app.jadpo`, where a review sees it, never by a setting.  next: Remove `tls: true` (agent)  rule: connection.value_invalid  help: https://jadpo.dev/docs/errors/connection.value_invalidcheck failed: 1 error, 2 files

An authority is never checked without TLS, so a connection can't have both (connection.key_invalid):

example
application {    connections: {        main: {            driver: Driver.postgres            url: config.database_url            authority: config.database_authority            tls: false        }    }}
text
error: `main` names an authority and goes without TLS --> app.jadpo:6:13  |6 |             authority: config.database_authority  |             ^^^^^^^^^ never checked  reason: `authority:` is what the server's certificate is checked against, and a connection with `tls: false` never asks for one.  next: Remove `authority:`, or `tls: false` (human)  rule: connection.key_invalid  help: https://jadpo.dev/docs/errors/connection.key_invalidcheck failed: 1 error, 2 files

PostgreSQL through Hyperdrive

On Cloudflare Workers, your app reaches PostgreSQL through Cloudflare's Hyperdrive, and you add nothing to app.jadpo for it. A Worker opens a new connection for each request; Hyperdrive keeps connections to the database open between requests, which a Worker can't.

The connection's url: setting names the binding. On Workers, that setting's variable (DATABASE_URL, or the setting's binding:) is a Hyperdrive binding, so the same app.jadpo runs natively and on Workers. jadpo build writes that binding into a Workers deployable's Wrangler file in place of a secret. It takes the id of the Hyperdrive configuration from JADPO_HYPERDRIVE_<variable> (A Workers deployable).

  • The address is the binding's. It holds the database's user, password and name at a host the Workers runtime makes for it, which connect() reaches inside Cloudflare. A text binding of that name is still an address the Worker reaches directly, as it would natively.
  • TLS is Hyperdrive's. The Worker reaches Hyperdrive without TLS: Hyperdrive's own address for the binding says sslmode=disable, its socket offers no TLS, and the connection doesn't leave Cloudflare. Hyperdrive uses TLS to the database, checking the server's certificate against the public authorities by default, as Jadpo does. A connection whose authority: names a private authority needs that authority uploaded to Hyperdrive, and the configuration created with it (--ca-certificate-id, --sslmode verify-full).
  • A direct connection. A connection a Worker makes directly, not through Hyperdrive, uses TLS through startTls(), checked against the platform's roots. A certificate refused there is the same fault naming the server as natively, though Workers reports it only once the first bytes go out.
  • Caching off. Hyperdrive caches reads and doesn't clear them when a write changes the data, and Jadpo's reads are always current (Reads and writes). So the configuration must be created with caching off (--caching-disabled). A Worker can't see a configuration's caching, so jadpo build says so when it writes a binding with no id.
  • No session state. Hyperdrive hands a Worker a connection for one transaction at a time, so Jadpo's statements are unnamed, each transaction sets its isolation level in its own BEGIN, and leases are rows, never advisory locks. That is the same on every target.

Not built yet: checking a configuration's caching on a real deploy.

Actions on D1

D1 is Cloudflare's SQLite database. It has no interactive transaction: it refuses BEGIN TRANSACTION. The one way to commit several statements together is a batch, which runs them in order as one SQL transaction and rolls the whole batch back when any of them fails. So an action on D1 runs its reads first and then sends all its writes at once:

  • Reads first. The action's reads run before any of its writes, each a statement of its own, and every record it loads is read with its row version, a number that changes whenever the row does. Reads go to the primary database, as D1's do outside its Sessions API, which an action doesn't use, so they are current (Reads and writes).
  • Writes as one batch. The action's writes, those of the public actions it calls, and its outbox and delivery records go out together as one batch, so they commit together or not at all (Logic).
  • Each write checked against the version it read. On PostgreSQL a write compares the row version PostgreSQL keeps, xmin (Outside calls hold no transaction). SQLite has no xmin, so each entity's table on D1 keeps a row version of its own in a column, which every write to the row changes. An update, delete or transition compares the version the action read. A statement in a batch that changes no row isn't a failure, so the driver makes a version that doesn't match fail the batch, which then rolls back whole.
  • A conflict reruns the action. A version that doesn't match is a concurrent change like any other. The whole action runs again with fresh reads, within the same budget of three attempts, and its keyed calls repeat under their keys (All-or-nothing actions and concurrent changes).
  • What can't be written that way is refused. For a D1 store only, jadpo check refuses an action whose writes can't all wait until the end, since a batch can only roll back whole. That is a write whose input depends on an earlier write in the same action, a read after a write, or a write refusal the action catches and carries on from. The same action on PostgreSQL is accepted.

A batch counts as one query against D1's limit per invocation, but each statement in it keeps D1's own limits: 100 bound parameters and 100 KB of SQL. The whole batch has 30 seconds.

Not built yet: D1 itself. This is the rule its driver is built to.

Markdown for agents: /docs/stores.md.Built from Jadpo main at a707efb.