# The backend language for agents to write and people to trust.

Jadpo is an experimental programming language for web backends written by AI agents and reviewed by people.

A secret in a response, a private field that leaks, a route that skips sign-in: in Jadpo these are compile errors, and your agent fixes them before you see the change. When the only fix is to loosen who may see what, the error says it's your call.

[Try it with your agent ](#try)[Learn it yourself](/learn)

You ask your agent to **show partners their details**. It sends back their API key too.

`main.jadpo` (the whole file):

```jadpo
type PartnerName = Text {
    max_length: 100
}

/// What the partners page shows of a partner.
type PartnerProfile = Output {
    name: PartnerName
    partner_key: ApiKey
}
```

Output of `jadpo check`, check failed: 1 error, 1 file:

error `PartnerProfile.partner_key` holds an `ApiKey`

- **reason:** An `ApiKey` is a secret value: it is never sent, logged, stored or shown. A response is written out, logged by proxies and kept by browsers, and a client never needs a secret back.
- **next:** Remove `partner_key`, and send what the client needs instead, such as whether one is set (agent)
- **rule:** [type.secret\_exposed](/docs/errors/type.secret_exposed)

Real code and compiler output. Each check the compiler makes has its own page, and this one is [type.secret\_exposed](/docs/errors/type.secret_exposed). The request to your agent is made up. The next step ends in `(agent)`, so your agent fixes this one itself, before you see the change. 

## The common holes agents leave are compile errors

In most backends, the rules for who may see what are spread across middleware, query code and the reviewer's memory. An agent can miss any of them. In Jadpo those rules are part of the program, so the compiler checks them.

### Private data that leaks by a side door

A note's owner and its reviewer may both read it. Only the reviewer may read its `appraisal`. The query `find_note` answers both of them, so sending `appraisal` from it is a compile error.

The indirect routes are errors too: copying the value into a variable, another field or an event, or using it in an `if`. A value keeps its field's rules wherever it goes.

`main.jadpo` (excerpt):

```jadpo
entity Note {
    id: Uuid
    owner_id: User.id { … }
    reviewer_id: User.id { … }
    text: NoteText
    appraisal: NoteText {
        policy: {
            read: [reviewer_id]
        }
    }

    policy: {
        read: [owner_id, reviewer_id]
    }
}

type NoteView = Output {
    pick: [Note.id, Note.text, Note.appraisal]
}

query find_note(note_id: Note.id) fails NoteNotFound -> NoteView {
    from: Note
    where: Note.id == note_id
    missing: NoteNotFound
}
```

Output of `jadpo check`, check failed: 1 error, 1 file:

error `find_note` sends `Note.appraisal`, which not every caller may read

- **next:** Return an output without `appraisal` from `find_note` (agent)
- **rule:** [policy.output\_field\_unproved](/docs/errors/policy.output_field_unproved)

Real code and output from [policy.output\_field\_unproved](/docs/errors/policy.output_field_unproved); the settings on the two user fields (`{ … }`) and the `User` type are left out. That page shows each side door being caught.

### Someone else's record, fetched by ID

An entity is a kind of stored record, like a database table with types. Its `policy:` says who may read and change its records. The compiler adds that policy to the SQL `WHERE` of every query on the entity, so no query can leave it out.

A record the caller may not see looks exactly like a missing one, so nobody learns that it exists.

`main.jadpo` (entity):

```jadpo
entity Article {
    id: Uuid
    title: ArticleTitle

    policy: {
        read: [Access.authenticated]
    }
}
```

The compiler also refuses a secret, such as an API key in the app's settings, used where it could leave the server: see [configuration.secret\_flow](/docs/errors/configuration.secret_flow). Every route needs sign-in unless it says otherwise: see [routes are signed in by default](/docs/routes-signed-in-by-default). And an app can't call an outside service it hasn't declared, because Jadpo has no general HTTP client. Each service comes in as a typed package: see [external services](/docs/external-services).

## When the fix is your call, the error says so

Some errors can only be fixed by loosening who may see or change data. The compiler can't know whether you want that, so it marks the next step `(human)`: the decision is yours, and the prompt below, which you paste into your agent, tells it to stop and ask you. If your agent loosens access anyway, `jadpo check` fails until a person adds an acceptance: a short file in the change that says why they accept it.

You ask your agent to **make articles shareable**. It opens the route to everyone.

`main.jadpo` (excerpt):

```jadpo
routes Articles {
    prefix: /articles

    GET /{article_id: Article.id} {
        authentication: RouteAuthentication.none
        output: ArticleView
        run: find_article(article_id)
    }
}
```

Output of `jadpo check`, 1 error:

error `GET /articles/{article_id}` lets anyone call it, but not everyone may read `Article`

- **next:** Add `Access.public` to `Article`'s `read:`, or remove `authentication: RouteAuthentication.none` from the route (human)
- **rule:** [policy.public\_grant\_missing](/docs/errors/policy.public_grant_missing)

**Your call:** should anyone on the internet be able to read articles?

Real code and output from [policy.public\_grant\_missing](/docs/errors/policy.public_grant_missing). The request is an example.

## What your agent actually writes

One feature, start to finish: a customer orders a cake. The app charges the card, stores the order and announces it with an event, a message other parts of the app can react to. These are real files from a small shop example, checked with the rest of its project. Only their imports are left out.

### What may come in

`Quantity` is a whole number from 1 to 12, because the oven holds twelve. A value outside that range can't become a `Quantity`, so the request is refused before any of the shop's code runs.

`Sterling` is this shop's name for an amount in pounds, held as whole pence. `Money` works in any ISO currency, and adding two different currencies is a compile error.

`domains/shop/types/quantity.jadpo` (type):

```jadpo
/// How many cakes one order is for: the oven holds twelve.
type Quantity = Int {
    min: 1
    max: 12
}
```

`domains/shop/types/sterling.jadpo` (type):

```jadpo
/// An amount in pounds, held as whole pence.
type Sterling = Money {
    currency: Currency.gbp
    rounding: Rounding.half_even
}
```

### The route

A route is one HTTP endpoint. This one takes a `PlaceOrder` and runs `place_order`, the action shown below. It passes the signed-in customer, never a customer ID from the request.

Each failure the action declares has a kind, such as not found or invalid, and the kind sets its HTTP status: 404 or 422 here. The compiler also adds them to the API's route list, a file kept with the code that lists every route and what it can return.

`domains/shop/routes/orders.jadpo` (routes):

```jadpo
routes Orders {
    prefix: /orders

    // The action's failures become HTTP errors by their kind (404, 422) and
    // are listed in api/main.jadpo_record. One it left undeclared wouldn't compile.
    /// Places an order.
    POST / {
        input: PlaceOrder
        output: OrderReceipt
        status: SuccessStatus.created
        run: place_order(input, current_principal.user)
    }
}
```

### The action

An action is a function that can change stored data and call outside services. This one charges the card, stores the order and announces it. The input type sits in the same file as the action that takes it.

-   **Units add up exactly.** Grams, kilograms and pounds combine as one `Mass`.
-   **Money keeps its currency.** Goods, postage and VAT are all `Sterling`, so another currency can't be added to them.
-   **Every failure is declared.** One the action forgets to list is a compile error.
-   **The payment provider's decline becomes the app's own failure,** `CardDeclined`.
-   **The order and its event are saved together,** or not at all. The action's database writes share one transaction, and the event is stored with them, so no event announces an order that wasn't saved. The charge goes first, with a key that stops a retry charging the card twice.

`domains/shop/actions/place_order.jadpo` (action):

```jadpo
/// What placing an order takes; a quantity outside 1 to 12 never gets in.
type PlaceOrder = Input {
    cake_id: Cake.id
    quantity: Quantity
}

/// Charges the card, records the order and announces it.
action place_order(
    input: PlaceOrder,
    customer: Principal.user
) fails CakeNotFound, CardDeclined -> OrderReceipt {
    var cake: Cake = attempt orderable_cake(input.cake_id)
    // Cakes in grams, box in kilograms, packing in pounds: one `Mass`.
    var weight: Mass = cake.weight * input.quantity + 1.2kg + 3lb
    // Dividing a `Mass` by 1kg gives a plain number: how many kilograms.
    var kilograms: Decimal = weight / 1kg
    var per_kilo: Sterling = Sterling(money.from_minor(200, Currency.gbp))
    var goods: Sterling = Sterling(cake.price * input.quantity)
    var postage: Sterling = Sterling(per_kilo * kilograms)
    // `* 120%` adds 20% VAT to the goods and the postage.
    var total: Sterling = Sterling((goods + postage) * 120%)
    // The provider's decline becomes this app's own failure.
    attempt pay.charges.create(
        AcmePay.ChargeRequest {
            amount_minor: money.minor(total)
        }
    ) {
        declined_card: CardDeclined
    }
    // The order and the event commit together, or not at all.
    var order: Order = Order.create {
        set: {
            customer_id: customer.id
            cake_id: cake.id
            quantity: input.quantity
            total: total
        }
    }
    emit OrderPlaced {
        order_id: order.id
    }
    return OrderReceipt {
        id: order.id
        total: order.total
    }
}
```

### Who may see the order

Orders belong to their customer. Only the customer an order names may create or read it. The compiler adds that rule to every query on `Order`.

`domains/shop/entities/order.jadpo` (entity):

```jadpo
entity Order {
    id: Uuid
    customer_id: Customer.id {
        on_delete: OnDelete.restricted
    }
    cake_id: Cake.id {
        on_delete: OnDelete.restricted
    }
    quantity: Quantity
    total: Sterling

    policy: {
        create: [customer_id]
        read: [customer_id]
    }
}
```

## A whole language, with the backend built in

Jadpo isn't a linter over TypeScript. It's one language and one compiler, and the parts of a backend you'd normally choose and connect are already in it. There's no framework, ORM, validation library or job queue to pick, so there's nothing for an agent to connect wrongly. Everything below works today.

-   **Strict types**
    
    Named types with limits (a number from 1 to 12, say), a separate ID type for each record, enums, units that convert exactly, and money in any ISO currency. [Types](/docs/values-and-types)
    
-   **HTTP APIs**
    
    Typed routes with one input and one output, an OpenAPI document for each API, and safe retries and time limits on every route. [HTTP](/docs/http)
    
-   **Data**
    
    PostgreSQL tables with references and constraints, migrations checked against the schema, indexes worked out from your queries, full-text search, and every write in a transaction. [Data](/docs/data)
    
-   **Sign-in and access**
    
    Sessions, tokens, API keys for other services, magic links, passkeys, and sign-in through a provider such as Google. Then who may see what: owners, roles, each customer's data kept apart on one backend, and permissions on single fields. A change that loosens access fails the check until a person accepts it. [Identity](/docs/identity)
    
-   **Outside services**
    
    Each outside service is a typed package, made by hand or imported from an OpenAPI document. Webhooks, recorded responses and generated fakes for tests, with a package manager and a lock file. [Integrations](/docs/external-services)
    
-   **Background work**
    
    Events saved in the same transaction as the data, subscribers, retries, schedules, and batches that pick up where they stopped. [Jobs and events](/docs/jobs-and-events)
    
-   **Settings and time**
    
    Typed settings and secrets, time zones, recurring times and translations. [Configuration](/docs/configuration-and-time)
    
-   **Testing**
    
    Tests for actions, routes and schedules, a real database for each test, a clock you control and a fake for every outside service. [Testing](/docs/testing)
    
-   **Tooling**
    
    A formatter that decides all layout, editor support through a language server, `--json` on every command, and the same shape for every error. [Tooling](/docs/tooling)
    
-   **AI in your app**
    
    Prompt files the compiler checks, typed answers from the model, streaming, token budgets, and similarity search with pgvector. [AI](/docs/ai)
    

1.  **.jadpo source**what your agent writes
2.  **One map of the whole app**every declaration, call and decision, checked as one program
3.  -   **Checks and reports**jadpo check, the permissions report, editor tooling
    -   **Interpreter**runs jadpo dev and jadpo test
    -   **Generated Rust**a native binary, or WebAssembly for Cloudflare Workers

Every tool reads the same map, so the checker, the editor and the build agree. Jadpo's CI runs every test both interpreted and compiled, so the two can't drift apart.

## Fast, and measured in the open

Apps compile to native code, or to WebAssembly for Cloudflare Workers, tested here in workerd, the open-source runtime behind Cloudflare Workers, running locally. The figures come from one app written twice: [in Jadpo](https://github.com/jadpo-dev/jadpo/tree/main/benchmarks/golden-todo), and [in strict TypeScript](https://github.com/jadpo-dev/jadpo/tree/main/benchmarks/golden-todo/typescript) with Hono, Zod, Drizzle, CASL and JOSE. It manages todos, but it's built like a real backend, not the five-minute demo.

-   **Sign-in**
    
    Browser sessions, API tokens, keys for other services that call it, and tokens from an identity provider. Every route needs sign-in except the health check.
    
-   **Permissions**
    
    Each person reads and changes only their own todos. A request with a missing or forged credential, or two that disagree, is refused.
    
-   **Data**
    
    PostgreSQL with migrations, edits that change only the fields sent, and paged lists that stay stable while the data changes.
    
-   **Reminders**
    
    A schedule finds overdue todos, and background jobs send reminders through a mail service, safely if a job runs twice.
    
-   **Deletes**
    
    A deleted todo is kept for 30 days, then a purge job removes it in batches.
    
-   **Running it**
    
    Typed settings and secrets checked before the app starts, rate limits on every caller, and a readiness check.
    

Before anything is timed, both versions must pass the same [44 tests over HTTP](https://github.com/jadpo-dev/jadpo/tree/main/benchmarks/golden-todo#readme), covering everything above. The timed requests read, add and list todos with all of it switched on: sign-in, permission checks, rate limits and transactions. Every response, and every row saved, is checked against what it should be. PostgreSQL 16 runs on the same machine. Each figure is the median of three rounds, and lower is better.

CPU per request

p95 latency

Workload

Jadpo

Bun

Node

Jadpo

Bun

Node

Read one todo

0.11 ms

0.19 ms

0.20 ms

4.5 ms

13.2 ms

14.1 ms

Add one todo

0.12 ms

0.47 ms

0.39 ms

6.4 ms

26.5 ms

24.4 ms

List 50 todos

0.35 ms

1.06 ms

2.18 ms

11.3 ms

65.1 ms

97.4 ms

Cold start

21 ms

109 ms

419 ms

–

–

–

CPU per request

p95 latency

Workload

Jadpo

TypeScript

Jadpo

TypeScript

Read one todo

0.65 ms

1.25 ms

9.4 ms

27.1 ms

Add one todo

0.71 ms

1.80 ms

10.3 ms

32.9 ms

List 50 todos

1.11 ms

2.69 ms

15.8 ms

42.4 ms

-   Jadpo is slower to start under workerd: 70 ms against TypeScript's 65 ms.
-   Jadpo on workerd is slower at background work than TypeScript on Bun. It delivers 2,239 events a second against Bun's 4,017, and each scheduled run takes 55 ms at p95 against 29 ms. When each event calls another service, Jadpo delivers 1,435 a second, 63% of what Bun manages on the same test.
-   Read with care. A native Jadpo app uses a thread per core, while Bun, Node and workerd each use one thread. So compare CPU per request first: the extra threads help native's latency too. Every run was on one shared Apple M1 Pro, and each test client waited for an answer before sending its next request, so these figures don't show production capacity.

From [Jadpo's benchmark summary](https://github.com/jadpo-dev/jadpo/blob/main/docs/compiler-runtime.md#211-performance), 29 Oct 2026. [How each one is run, and how to reproduce it](https://github.com/jadpo-dev/jadpo/blob/main/benchmarks/README.md).

## Change one thing, and the whole app is checked again

An agent sees the file it's editing, not everything that depends on it. Any compiler checks types across files. Jadpo's also checks three things that usually break quietly: the failures callers must handle, the database schema and the public API.

### A new way to fail

When an action can fail in a new way, every caller must handle the failure or pass it on. None can quietly ignore it.

### A schema change

Your entities describe the database you want. The check fails until a migration says how to get there from the last version.

### A changed API

Each API has a route list kept with the code. A change to a route fails the check until you update the list with `jadpo api update`, so the change shows up in the diff a person reads.

Output of `jadpo check`, check failed: 1 error, 1 file:

error `RegistrationClosed` can leave `register`, but its `fails` list doesn't say so

- **reason:** `fails` lists exactly the failures that can leave an action, so every caller knows what it must handle.
- **next:** Add `RegistrationClosed` to `register`'s `fails` list (agent)
- **rule:** [failure.undeclared\_propagation](/docs/errors/failure.undeclared_propagation)

## Fair questions

The ones a sceptical reader asks first, answered plainly.

### What stops my agent just adding `Access.public`?

`jadpo check` does. Making data readable by callers who couldn't read it before fails the check until the change adds an acceptance: a short file in `acceptances/` where a person writes why they accept it. Your agent can draft the file, but the check keeps failing until the reason is written. An agent could write a reason itself, so on GitHub, list `acceptances/` in CODEOWNERS and protect your main branch. Then the change can't merge without your approval.

### Why not Postgres row-level security, or TypeScript and lint rules?

Those each check one layer. Jadpo checks routes, queries, response fields, secrets, failures and which customer owns each record as one program, so an open route that reaches private data fails before it runs. Jadpo is the experiment that finds out whether that's worth a new language.

### How do I review what my agent changed?

With your usual diff. Jadpo also writes files into your repository that the diff shows changing: `access.jadpo_record` lists who may read and change each kind of stored data and each field, and each API has a file listing its routes. A change that loosens access or breaks existing API clients must also add an acceptance with a person's reason.

### What does it run on?

PostgreSQL for data. Apps compile to a native binary, or to WebAssembly for Cloudflare Workers. The compiler is written in Rust.

### Can I drop down to TypeScript or raw SQL?

No, and that blocks things real apps need, such as hand-written SQL for an unusual report. Jadpo code has no raw SQL and no general HTTP client. Outside services come in as typed packages.

### Can my agent really write a language this new?

That's what the docs are for. There's a one-page primer, a step-by-step path that builds one app, a page for every compiler error, and a Markdown copy of every page. Every error says what to do next.

### Who's behind it?

One person, working with coding agents. It's a research experiment, meant to find out quickly whether it beats a strong TypeScript baseline.

## What the compiler can't tell you

It makes a common class of bugs hard to write. It doesn't make software correct.

-   Whether the requirements are right.
-   Whether the policy a person approved is right.
-   Whether the business logic says the rule you meant.
-   Whether providers and infrastructure stay up and uncompromised.
-   Whether Jadpo's own compiler and runtime are free of bugs.

## Not built yet

Everything else on this page works today. These don't yet, or only partly, and here's what to do instead.

-   A plain-language summary of what a change loosenedRead the diff of `access.jadpo_record`, the file Jadpo writes listing who may do whatplanned
-   Handling personal dataJadpo can erase a person and delete old records, though not yet in every case, such as records holding files. It can't export a person's data yetpartly
-   Raw SQL or TypeScript inside an appNot on the roadmap. Keep that work in a separate servicenot planned
-   Running on Cloudflare itselfWorkers builds pass local tests in workerd, and performance is measured therenot tried
-   How agents do in Jadpo against TypeScriptThe test and its 18 changes are written; the runs haven't been donenot measured

[What's built, area by area](/status)

## Try it with your agent

Jadpo is new, so your agent learns it from these docs. The prompt has your agent install the CLI and start a project, so read it before you paste it.

`Paste into your agent`:

```text
Help me try Jadpo, an experimental language for AI-written backends. It's new and changes between versions, so don't rely on what you remember about it. Start at https://jadpo.dev/llms.txt and read https://jadpo.dev/docs/primer.md before writing any Jadpo.

Install the Jadpo CLI as https://jadpo.dev/install.md describes, start a project with jadpo new, then run jadpo check and jadpo test in it.

When an error's next step ends in (human), stop and ask me before changing anything. For other errors, read the error's page, fix it, and tell me what the compiler caught.
```

-   [llms.txt](/llms.txt) is the one starting point for agents: what Jadpo is, where the primer is, and how to find any page by topic or error.
-   These docs follow Jadpo's `main` branch. Each release has its own copy of the docs, and llms.txt says how to find the one for your version.

### Try it yourself

-   [Learn Jadpo](/learn)
    
    Build a todo app with sign-in in 14 pages, from an empty folder to a deployed server. Start here.
    
-   [Install the CLI](/install)
    
    With npm, as here, or Homebrew, an install script or a prebuilt binary.
    
    $ npm install -g @jadpo/jadpo
    $ jadpo new
    
-   [Keep the primer beside you](/docs/primer)
    
    The whole language on one page, once you've started.
