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: External services

External services

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. 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.

example
// 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:

example
// 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: 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). 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), 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) 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 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.

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) 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:

example
// 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.