# Capabilities

A **capability** is a job Jadpo does itself, such as storing files or
sending email, done through a connection you choose. Your code uses the
capability's one API, never the provider's, so changing provider changes
`app.jadpo` and never your domain code.

A capability exists only for a job Jadpo performs itself, and it covers
that job whole. Jadpo owns the list. Every connection that fills a
capability does all of it, so there are no optional parts and partial
support can't arise.

| Capability | Code uses | Built-in drivers | Packages |
|---|---|---|---|
| `database` | Entities ([Data](data.md)) | PostgreSQL; D1 later | No |
| `cache` | `caches:` ([Caches](caches.md)) | Redis (Upstash, Valkey and ElastiCache through it, on Workers too); in memory, a warning in production and an error on Workers | No |
| `files` | `File` fields, `store.read` and `store.put` ([Files](files.md)) | S3 (R2, B2, MinIO and the rest through its settings); disk, native only, one server | Only one that does the whole job |
| `email` | `type X = EmailMessage`, emitted, and Jadpo's own emails ([Email](email.md)) | SMTP; the local inbox | Yes |
| `publishing` | Chosen public events sent outward after commit; outgoing webhooks | The local log; outgoing webhooks | Yes |
| `receiving` | Queue messages arriving as events | Cloudflare Queues on Workers; Kafka later | Yes (SQS, Pub/Sub pull) |
| `schedules` | `schedule` ([Schedules](schedules.md)) | Timer; Cloudflare cron; external tick | No |
| `search` | `search:` on an entity ([Search](search.md)) | PostgreSQL full-text search, the default | Yes (hosted engines) |
| `bot_check` | `bot_check: true` on a route or generated feature ([Bot protection](bot-protection.md)) | A local driver passing every check in development and tests | Yes (Turnstile, hCaptcha, reCAPTCHA) |
| `documents` | `type X = Document` rendered to HTML or PDF ([Documents](documents.md)) | None: tests fake it | Yes (Gotenberg as a service, DocRaptor, PDFShift) |
| `scanning` | Uploads scanned before they can be attached ([Built-in services](built-in-services.md)) | None | Yes (a ClamAV service, a scanning API); built when an app needs it |
| `ai` | Prompts and `ai.generate`, embeddings ([AI](ai.md)) | OpenAI-compatible servers, for local models | Yes (`provides/ai.jadpo`: Anthropic, OpenAI and the rest) |

Some things are not capabilities:

- **Event delivery** stays inside the runtime, from the outbox in the main
  store, so logs and traces stay in one place and no queue service is
  needed.
- **Telemetry** has its own section in `application { }` and one
  protocol, OpenTelemetry ([Telemetry](telemetry.md)).
- **Feature flags, rate limits, sessions and idempotency keys** each name
  a store.
- **SMS**, because nothing in the language sends SMS. Twilio is a
  package, and its text is still typed against translations. `sms` comes
  back as a capability if SMS sign-in codes are built.

## One `connections:` block

Every connection is an entry in one `connections:` block, and each entry
is a built-in driver or a package. Where a connection is used says which
capability it serves. One connection is one resource, such as a bucket or
a queue, not one vendor account, as Cloudflare's own bindings are.

- Entities, caches and `File` fields name a connection with `store:`.
- `email:`, `publishing:`, `receiving:`, `documents:` and `bot_check:`
  name one in `application { }`.
- `search:` names a search engine's connection, and PostgreSQL when it's
  left out.
- Schedules use the target's own tick unless told otherwise.

Using a connection for a capability its package doesn't provide is a
compile error.

```jadpo
// app.jadpo
application {
    connections: {
        postmark: {
            package: Postmark.v1
            domains: [marketing]
            credential: config.postmark_token
            environment: config.postmark_environment
            message_stream: "outbound"
        }
    }
    email: {
        connection: postmark
        from: config.mail_sender
    }
}
```

## What a provider does beyond the capability

Everything specific to one provider is the package's own operations,
called from an action or subscriber in a domain the connection lists, as
payments are. Examples are hosted templates with variables
(`postmark.email.send_with_template(...)`), suppression lists and
marketing sends. One connection serves both: `email:` uses it, and the
listed domains call it.

Bounces and complaints are the package's own webhook events
(`Postmark.BounceWebhook`), so moving to another provider means rewriting
the subscribers that read them. They become part of the capability only
when a built-in feature uses them.

Mail sent through a hosted template gives up the dev inbox entry, the
health score and the automatic template check. An app can add a contract
case to check it instead.

## How a package provides a capability

A file `provides/<capability>.jadpo`, found by its name, maps each
standard operation of the capability onto one of the package's
operations:

```jadpo
// packages/postmark/v1/provides/email.jadpo
provide email {
    send: {
        call: email.send
        input: {
            From: message.from
            To: message.to
            Subject: message.subject
            HtmlBody: message.html
            TextBody: message.text
            MessageStream: settings.message_stream
            Attachments: {
                each: message.attachments
                as: {
                    Name: item.name
                    Content: item.content
                    ContentType: item.content_type
                }
            }
        }
        receipt: response.MessageID
        failures: {
            inactive_recipient: EmailFailure.recipient_rejected
        }
    }
    limits: {
        attachments: 10mb
    }
}
```

- **Mappings stay data**: field paths, literals, lists, nested blocks,
  enum tables, and `each:` with `as:` to map a list element by element,
  the current element named `item`. A mapping has no expressions or
  calls. Any computing is done in the package's decoders and encoders.
  The compiler checks both sides of a mapping.
- **Email attachments are part of the capability.** A `File` field on an
  `EmailMessage` is an attachment. It is read from its store when the
  message is sent, and checked against the mapping's
  `limits: { attachments: }`, the total size the provider accepts. A
  provider that can't send attachments stays a package, but can't be the
  email connection.
- **Emails without an idempotency key.** Postmark, SES, SendGrid and
  Mailgun take no idempotency key, so an email whose sending ends in
  `outcome_unknown` is parked rather than sent again
  ([Delivery: at least once, safe to repeat](delivery-at-least-once-safe-to-repeat.md)).
  Sending two kinds of mail through two connections waits for an app that
  asks for it.
- **Image variants**, once built, are made by the runtime over any store,
  so `files` stays whole. Media services such as Cloudinary and Uploadcare
  stay ordinary packages, since they can't do the whole `files` job.

For `email` ([Email](email.md)),
`provides/email.jadpo` is read and checked against the package's
operation and settings (`package.provider_invalid`). Its `input:` keys
are the operation's input field names, as `provide receiving`'s are, with
the wire names coming from the types rather than written out as the
example above writes them. `email:` may name the connection, which then
needs no `domains:`. Each delivery calls the mapped operation with the
rendered message, and its failures are mapped onto `EmailFailure`'s. The
Resend package (Resend's `POST /emails`) is the second email connection
beside SMTP: moving between them changes `app.jadpo` and its settings
only.

## Erasing a person at a provider

When a person is erased, a provider that holds their data has to erase
it too. This isn't a capability mapping:

1. The package declares its erasure operation in `provider.jadpo`:
   `erasure: { call: customers.delete  by: CustomerId }`.
2. The connection names the app's field that holds the provider's key for
   the person: `erasure: { key: User.stripe_customer_id }`.
3. Jadpo copies each key into the erasure event, and the app erases the
   person in one transaction.
4. After commit, a generated subscriber calls each provider, retried
   until the provider confirms.

The erasure-completed email ([Erasure](erasure.md)) goes
only once every provider has confirmed. A delivery that gives up is
parked for an operator and shown, never left quietly incomplete. A copied
key that is personal data, such as an email address, stays only in the
outbox until it is delivered, and is listed in the personal-data report.

A connection that receives personal data but can't erase it is a compile
error, unless an acceptance ([Acceptances](acceptances.md)),
`ErasureNotPossible`, states why the data stays, such as tax records. It
is listed in the personal-data report. The app may add its own
`PersonErased` subscribers.

> Erasure at providers isn't built yet: it comes with
> [Personal data](personal-data.md).

## Proving a package works

Each capability has a conformance suite, a set of checks any package
filling it must pass. `jadpo package conform <package> <capability>` runs
it against the provider's sandbox or an emulator. The registry lists a
package for a capability only with a passing run pinned to the package's
digest, and the app's own integrations run the suite in scheduled CI.
Tests use one fake per capability, whatever the provider: `email.sent`
for email, `published` for publishing.

For `ai` ([AI](ai.md)), `jadpo package conform <package> ai` runs the
suite in an app. By default it runs against the package's recordings and
a local fake on this machine's loopback address. With `--live` it runs
against the provider's API through the app's own connection, and it is
refused (`conform.refused`) when there is no connection or its credential
isn't set.

> Only the `ai` suite is built. Other capabilities' suites, the run pinned
> to the digest for the registry, and the scheduled CI run aren't built
> yet.

## In development

In `jadpo dev` nothing leaves the machine: published events go to a log
in the dev console, search goes to PostgreSQL whatever production uses,
and the cache runs in memory. Where the other connections go in
`jadpo dev`, in tests and on each target is the table in
[How packages, connections, capabilities and services fit together](how-integrations-fit-together.md).

## Reports

- The manifest records each connection's capabilities, hosts, services
  and targets.
- The network report labels each host with its connection and
  capability.
- The personal-data report lists the connections that receive marked
  fields, which of them can erase, and the acceptances for those that
  can't.

## Targets

A target is where a deployable runs, natively or on Cloudflare Workers.
Each driver and package says which targets it runs on, and `jadpo check`
refuses a deployable that uses one elsewhere, naming what to use instead.

- A package that calls an HTTP API runs on every target.
- A service (`runs:`) runs only beside a native deployable, so on Workers
  its connection gives `host:`.
- Cloudflare Workers can open TCP sockets but block port 25, so SMTP
  works there only on port 587.
- Workers has no timer of its own, so its schedule tick is Cloudflare
  cron.
- R2 and Queues bindings exist only on Workers.
- The disk driver and an in-memory cache don't run on Workers.

A deployable ([Declared deployables](declared-deployables.md))
that declares a target is refused each connection it reaches whose driver
that target can't run, with `deployable.target_unsupported` naming what
to use instead. On Workers that is the disk, a service Jadpo runs without
`host:`, and SMTP on port 25. Natively it is a Cloudflare queue, which a
deployable reaches when it runs `Trigger.subscribers`, where the queues
`receiving:` reads are received.

What a deployable reaches is followed from the routes of the APIs it
serves and the background work it runs, through everything they call. An
app that declares no deployables, and a deployable with no `target:`,
have no target, so nothing is refused for them
([Tooling's design notes](https://github.com/jadpo-dev/jadpo/blob/main/docs/language/notes/14-tooling.md)).

> An in-memory cache is checked against the target once caches are built.

## Choosing a provider when deploying

An app that other people deploy, such as an open-source product run by
many operators, declares every connection it supports, and a setting
picks one when the app starts:

```jadpo
// app.jadpo
email: {
    connection: config.mail_provider
    from: config.mail_sender
}
```

Every candidate connection is compiled, checked and reported, and one
that isn't chosen needs no credential. Sending different messages through
different connections, and falling back to a second provider, come later,
and only when an app asks for them.

> Choosing a provider when deploying isn't built yet. It is built when
> the first app that other people run needs it.
