Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 1 Nov 2026. What's built · Install 0.2.0
jadpo

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.

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

main.jadpothe whole file
type PartnerName = Text {    max_length: 100}/// What the partners page shows of a partner.type PartnerProfile = Output {    name: PartnerName    partner_key: ApiKeyadded by your agent}
jadpo checkcheck 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

Real code and compiler output. Each check the compiler makes has its own page, and this one is 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.jadpoexcerpt
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}
jadpo checkcheck 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

Real code and output from 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.jadpoentity
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. Every route needs sign-in unless it says otherwise: see routes are 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.

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.jadpoexcerpt
routes Articles {    prefix: /articles    GET /{article_id: Article.id} {        authentication: RouteAuthentication.noneadded by your agent        output: ArticleView        run: find_article(article_id)    }}
jadpo check1 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

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

Real code and output from 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.jadpotype
/// How many cakes one order is for: the oven holds twelve.type Quantity = Int {    min: 1    max: 12}
domains/shop/types/sterling.jadpotype
/// 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.jadporoutes
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.jadpoaction
/// 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.jadpoentity
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

  • 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

  • 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

  • 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

  • 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

  • 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

  • Settings and time

    Typed settings and secrets, time zones, recurring times and translations. Configuration

  • 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

  • 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

  • AI in your app

    Prompt files the compiler checks, typed answers from the model, streaming, token budgets, and similarity search with pgvector. AI

  1. .jadpo sourcewhat your agent writes
  2. One map of the whole appevery declaration, call and decision, checked as one program
    • Checks and reportsjadpo check, the permissions report, editor tooling
    • Interpreterruns jadpo dev and jadpo test
    • Generated Rusta 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, and in strict 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, 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.

Jadpo native against TypeScript on Bun and Nodelower is better
CPU per requestp95 latency
WorkloadJadpoBunNodeJadpoBunNode
Read one todo0.11 ms0.19 ms0.20 ms4.5 ms13.2 ms14.1 ms
Add one todo0.12 ms0.47 ms0.39 ms6.4 ms26.5 ms24.4 ms
List 50 todos0.35 ms1.06 ms2.18 ms11.3 ms65.1 ms97.4 ms
Cold start21 ms109 ms419 ms–––
Jadpo as WebAssembly against TypeScript, both on workerdlower is better
CPU per requestp95 latency
WorkloadJadpoTypeScriptJadpoTypeScript
Read one todo0.65 ms1.25 ms9.4 ms27.1 ms
Add one todo0.71 ms1.80 ms10.3 ms32.9 ms
List 50 todos1.11 ms2.69 ms15.8 ms42.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, 29 Oct 2026. How each one is run, and how to reproduce it.

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.

jadpo checkcheck 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

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

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

    Build a todo app with sign-in in 14 pages, from an empty folder to a deployed server. Start here.

  • Install the CLI

    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

    The whole language on one page, once you've started.