# 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](external-services.md) 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:

```jadpo
// app.jadpo in a shop that keeps its orders apart from its catalogue
application {
    connections: {
        catalogue: {
            driver: Driver.postgres
        }
        orders: {
            driver: Driver.postgres
        }
    }
}
```

```jadpo
/// Something the shop sells.
entity Product {
    id: Uuid
    name: Text

    store: catalogue
}
```

```jadpo
/// 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](jobs-and-events.md)).
An entity in Redis must say when its records expire
([Entities in Redis](entities-in-redis.md)).

**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](identity.md));
- events and the outbox, the table that holds each event until it is
  delivered ([Jobs and events](jobs-and-events.md));
- idempotency keys ([Idempotent writes](idempotent-writes.md));
- webhook de-duplication ([Integrations](external-services.md));
- flags and audit ([Built-in services](built-in-services.md)).

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](database-and-other-connection-urls.md)).
`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](external-services.md).

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](stores.md#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](standard-library.md)). 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](connecting-in-app-jadpo.md#the-network-report) lists the
connection as reached without TLS.

```jadpo
application {
    connections: {
        main: {
            driver: Driver.postgres
            url: config.database_url
            authority: config.database_authority
        }
        legacy: {
            driver: Driver.postgres
            url: config.legacy_url
            tls: false
        }
    }
}
```

```jadpo
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](configuration-and-time.md)). 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`):

```jadpo
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_invalid

check failed: 1 error, 2 files
```

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

```jadpo
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_invalid

check failed: 1 error, 2 files
```

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

```jadpo
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_invalid

check failed: 1 error, 2 files
```

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

```jadpo
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_invalid

check 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](a-workers-deployable.md)).

- **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](reads-and-writes.md)). 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](reads-and-writes.md)).
- **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](logic.md)).
- **Each write checked against the version it read.** On PostgreSQL a
  write compares the row version PostgreSQL keeps, `xmin`
  ([Outside calls hold no transaction](all-or-nothing-actions-and-concurrent-changes.md#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](all-or-nothing-actions-and-concurrent-changes.md)).
- **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.
