External servicesWriting and publishing packages
The package format
On this page
A package is laid out as shared/ is, so one set of rules covers the app
and every package, and the compiler checks a package as it checks
shared/.
Here is the smallest useful package: its index, one type, one library and a
test beside it. An app holds its own package like this in
shared/providers/postcodes/v2/. Published, the same files are a repository
of their own, and an app that installs it finds them in
packages/postcodes/v2/. text.upper and text.trim are built in.
shared/providers/postcodes/v2/package.jadpo:
/// UK postcodes: tidying them as people write them.package Postcodes.v2 { licence: Licence.mit jadpo: "1.0"}
shared/providers/postcodes/v2/types/postcode.jadpo:
/// A UK postcode.public type Postcode = Text {}
shared/providers/postcodes/v2/libraries/uk.jadpo:
/// Tidying UK postcodes.public library uk { /// A postcode in capitals, with the spaces around it left out. function tidy(postcode: Postcode) -> Postcode { return Postcode(text.upper(text.trim(postcode))) }}
shared/providers/postcodes/v2/libraries/uk.test.jadpo:
test "a postcode is tidied" { assert uk.tidy(" sw1a 1aa ") == "SW1A 1AA"}
A library package in full:
postcodes/v2/ package.jadpo the index: description, licence, lowest Jadpo version types/uk_postcode.jadpo public type UkPostcode = Text { ... } failures/invalid_postcode.jadpo libraries/uk.jadpo public library uk { parse(...) normalise(...) } libraries/uk.test.jadpo tests beside the file, as in an app translations/ its own messages and failure wording GUIDE.md optional: how to use it well CHANGELOG.md surface.jadpo_record generated
A provider package adds provider.jadpo and the folders an API needs:
stripe/v2024_06_20/ package.jadpo the same keys as every package provider.jadpo how the API is reached and called resources/payment_intents.jadpo imported, corrections merged in types/payment_intent.jadpo imported failures/card_declined.jadpo events/payment_intent_succeeded.jadpo actions/start_subscription.jadpo written: several of its own calls in order subscribers/renewal.jadpo written: enriches its own webhook libraries/tables.jadpo written: decoders and helpers provides/email.jadpo only when it fills a capability recipes/track_parcels/ wiring examples an app copies in recordings/payment_intents.create.json real responses that confirm the types surface.jadpo_record
- The index lists no files. A file is named after what it declares, so the compiler finds a resource by name and loads only what the app uses.
- A re-import rewrites only imported files. Written libraries, actions and corrections survive.
- A package's tests run when it is published, not in every app's
jadpo test; the digest guarantees the code is the code that passed. - No
emails/folder. Email templates, partials and layouts are the app's own, and anemails/folder in a package is a compile error. - Translations are only in library packages, for their own messages
and failures, laid out as a domain's
(Packages' text: groups, calls and the surface).
An app calls them as
Postcodes.translations.<message>(...)and rewords them in its roottranslations/. Provider packages show users no text, because their failures are always mapped to the app's own, so they carry none. GUIDE.mdis optional, and says how to use the package well. Every code example in it is compiled and run when the package is published, andjadpo package show <name> --guideprints it.
The index: package.jadpo
Every package's index has the same form: package Name.vN { }, a ///
description above it, and its keys. licence: names its licence, jadpo:
the lowest Jadpo version it says it needs, and publisher: who publishes it in
the registry, as a handle: publisher: "@acme" for @acme/postcodes. Nothing
compares jadpo: with the running Jadpo yet (How the package format is
read).
A package only used by path or from git may leave publisher: out, since
nothing is published and no one is named. The publishing bar asks for it
(What publishing requires).
The licence is one of Licence.mit, .apache_2_0, .bsd_2_clause,
.bsd_3_clause, .isc, .mpl_2_0, .unlicense and .proprietary.
Only a package installed from git may be .proprietary; the public
registry refuses it
(publish.licence_proprietary). There is no requires:, because packages have no
dependencies, and no list of files.
// packages/postcodes/v2/package.jadpo/// UK postcodes: parsing, normalising and checking the outward code.package Postcodes.v2 { publisher: "@acme" licence: Licence.mit jadpo: "1.0"}
How the API is reached: provider.jadpo
A package that describes an API has one provider.jadpo: a
provider { } declaration with a fixed set of keys. A package without
one is a library.
One package describes at most one API, because one API is one connection with one set of hosts and credentials. A vendor with two APIs under different credentials, such as Shopify's Admin and Storefront APIs, is two packages.
| Key | Says |
|---|---|
protocol: |
How bodies are sent: Protocol.json_http, .form_http, .multipart_http, .aws_json. Required |
hosts: |
live: and sandbox:; the same host twice when the sandbox is a test credential; a host may hold one placeholder filled from a setting ("https://{project_ref}.supabase.co") |
runs: |
Not built yet: a container Jadpo runs instead of a fixed host: image: pinned by digest, port: it listens on inside the container, asks: the outbound hosts it needs (Services). It isn't the runs: on a deployable |
credential: |
The scheme (CredentialScheme.bearer, a named header, basic, AWS signing), which decides the shape of credential the app gives |
credential_header: |
For CredentialScheme.header, the named header the credential goes in alone ("x-api-key"); required for it and only for it |
headers: |
The fixed headers every request carries, each "Name: value" (["anthropic-version: 2023-06-01"]), never one the driver writes itself |
oauth: |
The provider's authorize, token and revoke addresses, for a connection whose credential is an OAuthGrant (see "Acting for each user") |
idempotency: |
header: (where the key goes), window: (how long the provider keeps a key; required), max_length:, the provider's error codes in_use: (the key's first request is still running), reused: (the key came with a different request) and new_key_on: (the first attempt failed and the key cannot be used again), and replays_failures: true (default false) for a provider that replays a failed answer under the same key. Left out when the API takes no key |
error_code: |
Where the provider's error code is in a failed response |
webhooks: |
check: (the signature scheme), event_id:, event_type:, occurred_at: where the body says when the event happened, header: for SignatureScheme.hmac_sha256, and handshake: where the provider needs one |
polls: |
Operations the runtime calls on a schedule, each with a default interval and the event emitted when the answer changes |
erasure: |
The operation that deletes a person at the provider and the key it takes (by:) |
max_concurrent: |
How many of its calls run at once per app instance |
settings: |
Typed values the app gives on the connection (message_stream:) |
imported: |
The description it was imported from, file:, that file's digest, digest:, and reviewed:, the package's types an agent has reviewed against the provider's documentation |
Where the API is comes from exactly one place:
hosts:when it is on the internet;runs:when Jadpo runs it, which isn't built yet;- neither, when the app gives
host:on the connection: another Jadpo app, a self-hosted product, or a service on Workers.
A connection's host: beside runs: means the service is already
running there, so Jadpo doesn't start it. A package with fixed hosts:
refuses a host:.
// packages/acme_couriers/v1/provider.jadpo/// Acme Couriers' booking and tracking API, written from its HTML docs.provider { protocol: Protocol.json_http hosts: { live: "https://api.acme-couriers.example" sandbox: "https://sandbox.acme-couriers.example" } credential: CredentialScheme.bearer idempotency: { header: "Idempotency-Key" window: 24h max_length: 255 in_use: "idempotency_key_in_use" reused: "idempotency_key_reused" } error_code: "error.code"}// packages/acme_couriers/v1/resources/bookings.jadpopublic resource bookings { create: { method: HttpMethod.post path: "/v1/bookings" input: CreateBooking output: Booking changes: true idempotent: true failures: [SlotTaken] source: PackageSource.written docs: "https://docs.acme-couriers.example/api#create-booking" }}// packages/acme_couriers/v1/types/create_booking.jadpopublic type CreateBooking = Object { postcode: Text slot: Text}// packages/acme_couriers/v1/types/booking.jadpopublic type Booking = Object { id: Text collect_at: Instant { wire: Wire.unix_seconds }}// packages/acme_couriers/v1/failures/slot_taken.jadpopublic type SlotTaken = Failure { kind: FailureKind.conflict message: "The collection slot is no longer available."}
idempotent: true on an operation means it is safe to send again: with
the package's idempotency: block, because it takes the key; without
one, because repeating it as it is changes nothing (a PUT that
replaces, a DELETE). An operation of a connection whose credential is
an OAuth grant also names the scope it needs, scope: "calendar.events".
Inside the package, its names are written bare. An app names them through
the package: AcmeCouriers.Booking, AcmeCouriers.SlotTaken. That holds
in a match arm too (AcmeCouriers.SlotTaken -> ...), and for an enum's
values, AcmeCouriers.Window.morning.
A failure's code, slot_taken from its name, is the provider's error
code. Jadpo reads it from the place in the response that error_code:
names.
What is public
publicmarks the package's API, as it marks what a domain offers other domains. Apublic resourceand apublic actioncan be called through the connection. Everything else can be called only by the package's own actions, subscribers and polls. As in an app,publicgoes on a library, never on a function: every function of apublic libraryis public, and internal helpers such as decoders live in a library withoutpublic(Project layout and modules). Internal declarations may change freely between versions. An importer marks every resource public.- Everything reachable from the public surface is public, and every
compile checks it. A failure a public library's function
failswith, a provider operation'sfailures:and the types in a public signature must themselves be public. Internal failures are mapped or handled before they leave a public declaration. A domain's public actions, queries and libraries follow the same rule. - A package's resources and actions share the connection's namespace
(
stripe.customers,stripe.start_subscription), so a clash between them is an error in the package. - Package events are public or not. The app may subscribe to a
publicevent; one withoutpublicreaches only the package's own subscribers, so a raw webhook a package enriches can stay internal while its enriched event is public. An importer marks webhook events public; subscribing to a package's non-public event is an error.
No presets, no dependencies
Built-in drivers are configured in app.jadpo, never packaged. A
driver is native code in Jadpo's runtime that speaks one protocol.
PostgreSQL, SMTP and OpenID Connect are drivers, and Redis and S3 will be. A
vendor that speaks one of them is that driver with the vendor's settings: Neon
is the PostgreSQL driver with its pooling mode, Gmail is the SMTP driver with
its host and port, and Cloudflare R2 will be driver: Driver.s3 with its host
and region: "auto". A vendor with storage and an API is a driver
connection plus a package. A driver for one vendor comes in a Jadpo
release only where a flow needs native code.
// app.jadpoavatars: { driver: Driver.s3 host: config.r2_host region: "auto" bucket: "avatars" credential: { key_id: config.r2_key_id secret: config.r2_secret }}
Driver.s3 isn't built yet, so this is its design and doesn't compile today.
- Each driver's page lists the settings for common services, so an
agent looks them up rather than guessing: R2, Backblaze B2,
DigitalOcean Spaces, MinIO and Amazon S3; Neon, Supabase and Amazon
RDS; Gmail, Microsoft 365 and Amazon SES's SMTP; Google and Microsoft.
Each is a working connection, checked with the pages' other examples.
jadpo checkwarns when it recognises a host that is missing one of its settings. - Sign-in providers are connections. Google, Microsoft and other
OpenID Connect providers are
google: { driver: Driver.oidc issuer: client_id: credential: }inconnections:, named bysign_in: { providers: [google, microsoft] }. Google's and Microsoft's settings are in Providers. GitHub and Apple aren't plain OpenID Connect, so they are the built-inDriver.githubandDriver.apple, which aren't built yet.
Packages have no dependencies: a package can't require another.
- The standard library holds the shared vocabulary, such as money, countries, currencies and phone numbers, which is what packages elsewhere mostly depend on each other for.
- Anything else is copied. A package that needs another's code includes a copy, checked and tested by the publishing bar like the rest of it.
- Two packages' types for the same thing are converted at the call, usually in one line.
- Installing a package is one folder and one line in the lock.
Markdown for agents: /docs/the-package-format.md.Built from Jadpo main at aee2e8f.