Stores
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:
// app.jadpo in a shop that keeps its orders apart from its catalogueapplication { connections: { catalogue: { driver: Driver.postgres } orders: { driver: Driver.postgres } }}
/// Something the shop sells.entity Product { id: Uuid name: Text store: catalogue}
/// 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:
- any entity stored in a database;
- sign-in and sessions (Identity);
- events and the outbox, the table that holds each event until it is delivered (Jobs and events);
- idempotency keys (Idempotent writes);
- webhook de-duplication (Integrations);
- flags and audit (Built-in services).
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.
application { connections: { main: { driver: Driver.postgres url: config.database_url authority: config.database_authority } legacy: { driver: Driver.postgres url: config.legacy_url tls: false } }}
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):
application { connections: { main: { driver: Driver.postgres url: config.database_url sslmode: "require" } }}
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):
application { connections: { main: { driver: Driver.postgres url: config.database_url authority: config.database_authority } }}
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):
application { connections: { main: { driver: Driver.postgres url: config.database_url tls: true } }}
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):
application { connections: { main: { driver: Driver.postgres url: config.database_url authority: config.database_authority tls: false } }}
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 whoseauthority: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, sojadpo buildsays 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 noxmin, 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 checkrefuses 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.