Capabilities
On this page
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) | PostgreSQL; D1 later | No |
cache |
caches: (Caches) |
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) |
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) |
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) |
Timer; Cloudflare cron; external tick | No |
search |
search: on an entity (Search) |
PostgreSQL full-text search, the default | Yes (hosted engines) |
bot_check |
bot_check: true on a route or generated feature (Bot protection) |
A local driver passing every check in development and tests | Yes (Turnstile, hCaptcha, reCAPTCHA) |
documents |
type X = Document rendered to HTML or PDF (Documents) |
None: tests fake it | Yes (Gotenberg as a service, DocRaptor, PDFShift) |
scanning |
Uploads scanned before they can be attached (Built-in services) | None | Yes (a ClamAV service, a scanning API); built when an app needs it |
ai |
Prompts and ai.generate, embeddings (AI) |
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). - 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.
smscomes 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
Filefields name a connection withstore:. email:,publishing:,receiving:,documents:andbot_check:name one inapplication { }.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.
// app.jadpoapplication { 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:
// packages/postmark/v1/provides/email.jadpoprovide 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:withas:to map a list element by element, the current element nameditem. 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
Filefield on anEmailMessageis an attachment. It is read from its store when the message is sent, and checked against the mapping'slimits: { 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_unknownis parked rather than sent again (Delivery: at least once, safe to repeat). 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
filesstays whole. Media services such as Cloudinary and Uploadcare stay ordinary packages, since they can't do the wholefilesjob.
For email (Email),
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:
- The package declares its erasure operation in
provider.jadpo:erasure: { call: customers.delete by: CustomerId }. - The connection names the app's field that holds the provider's key for
the person:
erasure: { key: User.stripe_customer_id }. - Jadpo copies each key into the erasure event, and the app erases the person in one transaction.
- After commit, a generated subscriber calls each provider, retried until the provider confirms.
The erasure-completed email (Erasure) 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),
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.
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), 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
aisuite 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.
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 giveshost:. - 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)
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).
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:
// app.jadpoemail: { 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.
Markdown for agents: /docs/capabilities.md.Built from Jadpo main at a707efb.