# Jadpo primer

Read this once before you write a Jadpo app. It shows how a project is
laid out, the syntax, and the parts nearly every app uses, and it ends
with a small app you can run on your own machine. Every example on this
page is compiled and tested, so you can copy it.

## What Jadpo is

Jadpo is a language for web backends: the server side of an app, which
stores its data and answers its HTTP requests. Coding agents write most
of it, and people review it.

One compiler checks the whole app at once. That covers the types, the
stored data, the HTTP routes, who can sign in, what each person may see
and change, the settings, calls to outside services, background work and
the tests. You write each rule once, in the place the language gives it,
and the compiler applies it everywhere.

Jadpo has no front end: a web page or a phone app calls it over HTTP.
It is at version 0.x, so a new minor release can still break your app.

Some habits from TypeScript and other languages don't carry over. The
compiler catches most of them and shows you the Jadpo way:

- No `null`. A type ending in `?` may hold `none`, and `match` reads it.
- No exceptions. A function lists what can go wrong with `fails`, raises
  a failure with `reject` and passes one on with `attempt`.
- No `async`, classes, methods, your own generics, `while`, `break` or
  floating point. `for` is the only loop, and `Decimal` is exact.
- No hand-written SQL. You declare queries and writes, and the compiler
  turns them into SQL, with each person's permissions added.
- No truthiness, no semicolons, and no `!`, `&&` or `||`: write `not`,
  `and` and `or`.

## The commands

Everything goes through one command, `jadpo`. Run it anywhere inside
your project: it finds the project by looking upwards for `app.jadpo`.

| Command | What it does |
|---|---|
| `jadpo new <name>` | Starts a project in a new folder, with a starter route and its test |
| `jadpo check` | Checks the whole project. It's quick, because it builds nothing |
| `jadpo fmt` | Formats your files, and adds, removes and sorts their `use` lines |
| `jadpo test` | Runs the tests. Tests that use the database need PostgreSQL installed |
| `jadpo migrate new <purpose>` | Writes the next database migration from your entities |
| `jadpo api update` | Updates the committed record of your API after you change a route |
| `jadpo dev` | Runs the app on `http://127.0.0.1:3000` and reloads it when you save |
| `jadpo help <command>` | Explains a command |

## Reading an error

Jadpo has no warnings. Everything it reports is an error, and the app
doesn't run until you fix it. Here a function returns a number its type
doesn't allow:

```jadpo
/// How many tools a member lends at once.
type ToolCount = Int {
    min: 0
    max: 20
}

/// The most a member may lend.
function limit() -> ToolCount {
    return 25
}
```

```text
error: `25` isn't a valid `ToolCount`
 --> main.jadpo:9:12
  |
9 |     return 25
  |            ^^ breaks `max: 20`
  reason: `ToolCount` declares `max: 20`, and the value is above 20. A literal is checked against every rule while compiling.
  next: Use a value that `ToolCount` allows (agent)
  rule: type.invalid_literal
  help: https://jadpo.dev/docs/errors/type.invalid_literal

check failed: 1 error, 1 file
```

Every error has the same parts:

- `reason:` says why the code is wrong.
- `next:` says what to do. `(human)` at the end of an error's `next:`
  line tells your coding agent to stop and ask you which fix you want,
  because the choice is yours to make (who can see data, what a setting
  holds). `(agent)` means your agent can make the fix itself. The marker
  is guidance that agents follow: nothing stops an agent that ignores it
  yet.
- `rule:` is the error's name. Its page, on the site at
  `https://jadpo.dev/docs/errors/<rule>`, explains it; read it only when
  the reason and the next step aren't enough.

`jadpo check --json` gives the same parts as JSON, for tools. A few rules
are judgement calls: when the code is right as written, a line
`decline <rule>: "<why>"` above the declaration answers that one finding.

## How a project is laid out

A project is a folder with `app.jadpo` at its top. Your code lives in
domains: one folder for each part of the business, such as `lending` or
`billing`, inside `domains/`. In a domain, each kind of declaration has
its own folder, such as `types/`, `entities/` or `actions/`. Each file
holds one declaration and is named after it.

This primer builds a small app for neighbours on one street to lend each
other tools. These are all the files it ends with:

```text
app.jadpo               the application and who can sign in
authentication.jadpo    how people sign in
config.jadpo            settings read from the environment
api/main.jadpo_record   generated by jadpo api update; committed
database/migrations/    generated by jadpo migrate new; committed
domains/
  lending/
    types/              tool_name.jadpo  tool_status.jadpo  tool_view.jadpo  loan_length.jadpo
    entities/           member.jadpo  tool.jadpo
    failures/           email_taken.jadpo  tool_already_lent.jadpo
    queries/            available_tools.jadpo  tool_by_id.jadpo
    actions/            join.jadpo  add_tool.jadpo  lend_tool.jadpo  lend_tool.test.jadpo
    routes/             tools.jadpo  tools.test.jadpo
```

A file starts with `use` lines naming where each name it uses comes from,
the domain and then the folder: `use lending/types { ToolName }` reads
`domains/lending/types/tool_name.jadpo`.
Built-in types and the standard library (`list.`, `money.`, `temporal.`)
are never imported. `jadpo fmt` adds missing imports and removes unused
ones, so run it after writing a file. A type that a second file uses must move to its own file in
`types/`; the compiler says so (`layout.promotion_required`) and
`jadpo fmt --layout` makes the move. A file outside any project is checked
on its own, with no layout and no `use` lines.

## Syntax essentials

```jadpo
/// A number of things, never below zero.
type Count = Int {
    min: 0
}

type Label = Text {
    max_length: 40
}

type Size = Enum {
    small
    large
}

type Parcel = Object {
    size: Size
    label: Label?
}

/// How many of the parcels are large.
function large_parcels(parcels: List<Parcel>) -> Count {
    var mut total: Count = 0
    for parcel in parcels {
        if (parcel.size == Size.large and parcel.label != none) {
            total = Count(total + 1)
        }
    }
    return total
}

/// What goes on the parcel's sticker.
function sticker(parcel: Parcel) -> Label {
    return match parcel.label {
        some(label) -> label
        none -> match parcel.size {
            Size.small -> "Small parcel"
            Size.large -> "Large parcel"
        }
    }
}
```

- Every type is declared `type Name = Base { ... }`. A block holds one
  item per line, with no commas; only a `use` line's names take commas.
  `///` documents the next declaration; `//` is a plain comment.
- Types are `UpperCamelCase`; everything else is `lower_snake_case`.
- Every variable states its type. `var` is like `const` and never
  changes; `var mut` can be given a new value. Values never change:
  build a new object instead.
- Conditions are in brackets and must be `Bool`. `match` must cover every
  case and can give a value.
- Arithmetic gives the plain representation (`Int`), so `Count(total + 1)`
  builds a `Count` again. Building a value checks its rules; it is never a
  cast.
- Order doesn't matter: a declaration can use one written later.

## Types

Representation is not meaning. A tool's name and a person's email are both
text, but they are different types, and the compiler never converts
between them silently. The built-in representations are `Bool`, `Int`,
`Decimal`, `Text`, `Bytes` and `Uuid`. About 250 standard types cover the
rest (`Email`, `Money`, `Instant`, `Duration`, `Url`, `PostalAddress` and
more), so look in the [standard library](topics/standard-library.md)
before inventing one.

```jadpo
/// What a tool is called, as its owner wrote it.
type ToolName = Text {
    min_length: 1
    max_length: 80
}
```

```jadpo
/// Whether a tool can be borrowed.
type ToolStatus = Enum {
    available
    lent
}
```

A function, action or query takes and returns named types, never a bare
`Text` or `Int`. The rules a type can carry are `min`, `max`,
`min_length`, `max_length`, `pattern` and `format`. A literal that breaks
them is a compile error. Building a computed value that may break them
needs `attempt` and a block mapping each rule that can break to a failure
(`max_length: NameTooLong`, or `invalid:` for all), unless the compiler
can prove the value follows them.

The bases for objects are `Object` (inside the app), `Input` (what a
request sends; its fields may be `optional: true` or have a `default:`) and
`Output` (what a response sends). Enum variants can carry fields, which
makes an enum a tagged union.

## Entities and migrations

An entity is a stored record: one table. Each has an `id: Uuid`. A
reference is the other entity's field type, `Member.id`, with what happens
on delete. `policy:` says who may do what (see "Signing in and
permissions").

```jadpo
/// Someone in the street who shares tools.
entity Member {
    id: Uuid
    email: Email {
        unique: true
    }
}
```

```jadpo
use lending/entities { Member }
use lending/types { ToolName, ToolStatus }

/// A tool a member lends to the others.
entity Tool {
    id: Uuid
    owner_id: Member.id {
        on_delete: OnDelete.cascade
    }
    name: ToolName
    status: ToolStatus
    due_back: Instant?

    policy: {
        create: [owner_id]
        read: [Access.authenticated]
        update: [owner_id]
    }
}
```

Each field is a type of its own: `Tool.id` and `Member.id` are both UUIDs,
but one can't be passed for the other. Every entity gets `Tool.get(id)`,
`Tool.create`, `Tool.update` and `Tool.delete`, and a not-found failure,
`ToolNotFound`, imported from `lending/entities`.

After changing an entity, run `jadpo migrate new <purpose>`. It writes a
dated folder in `database/migrations/` with a snapshot and the SQL. Commit
it. `jadpo check` fails while the migrations and the entities disagree.
When a change needs a decision, such as a value for a new field, the
folder gets a `changes.jadpo` to fill in, and `jadpo migrate new` run again
finishes it.

## Failures

A failure is an expected outcome, declared as a type. Its `kind` decides
the HTTP status; code never picks a status. Its public code is its name in
snake case, `tool_already_lent`. `client:` fields are sent to the caller;
`internal:` fields never leave the server.

```jadpo
use lending/entities { Tool }

/// The tool is out on loan, so it can't be lent again.
type ToolAlreadyLent = Failure {
    kind: FailureKind.conflict
    message: "That tool is already lent out."
    client: {
        tool_id: Tool.id
    }
}
```

```jadpo
/// Another member already uses this address.
type EmailTaken = Failure {
    kind: FailureKind.conflict
    message: "That email address is already a member's."
}
```

The kinds an app declares are `invalid_value` (422), `not_found` (404),
`not_permitted` (403), `conflict` (409), `rejected` (422), `rate_limited`
(429) and `unavailable` (503). The runtime raises the rest itself, such as
`unauthenticated` (401) and `internal_fault` (500), and they are never
listed in `fails`. Every failure reaches a client in one JSON envelope:
`{ "error": { "code", "message", "request_id", "details" } }`.

## Queries

Every read is a named query in `queries/`. The result type says how many
records come back: one (needs `missing:`), zero or one (`T?`), a list
(needs `order:` and `limit:`) or a `Page<T>`. A query reads only the fields
its result type needs.

```jadpo
use lending/entities { Tool }

/// A tool as members see it.
type ToolView = Output {
    pick: [Tool.id, Tool.name, Tool.status, Tool.due_back]
}
```

```jadpo
use lending/entities { Tool }
use lending/types { ToolStatus, ToolView }

/// Tools nobody has borrowed, by name.
query available_tools() -> List<ToolView> {
    from: Tool
    where: Tool.status == ToolStatus.available
    order: [Tool.name asc]
    limit: 100
}
```

```jadpo
use lending/entities { Tool, ToolNotFound }
use lending/types { ToolView }

/// One tool.
query tool_by_id(tool_id: Tool.id) fails ToolNotFound -> ToolView {
    from: Tool
    where: Tool.id == tool_id
    missing: ToolNotFound
}
```

## Actions and writes

A `function` is pure. A `query` only reads. An `action` may read, write,
call other actions and call outside services, and the whole action commits
or rolls back together. Only actions read the time, `clock.now`.

A write that can break a unique rule maps it in `conflict:`:

```jadpo
use lending/entities { Member }
use lending/failures { EmailTaken }

/// Makes someone a member.
action join(email: Member.email) fails EmailTaken -> Member {
    return attempt Member.create {
        set: {
            email: email
        }
        conflict: {
            Member.email: EmailTaken
        }
    }
}
```

An action that needs the caller takes it as a parameter of type
`Principal.user`, the signed-in user (see "Signing in and permissions").
An `Input` used by one action lives in the action's file:

```jadpo
use lending/entities { Tool }
use lending/types { ToolName, ToolStatus, ToolView }

/// What adding a tool takes.
type AddTool = Input {
    name: ToolName
}

/// Adds a tool the caller owns.
action add_tool(input: AddTool, caller: Principal.user) -> ToolView {
    var tool: Tool = Tool.create {
        set: {
            owner_id: caller.id
            name: input.name
            status: ToolStatus.available
            due_back: none
        }
    }
    return ToolView {
        id: tool.id
        name: tool.name
        status: tool.status
        due_back: tool.due_back
    }
}
```

`fails` lists exactly the failures that can leave a callable. `attempt`
starts a statement and passes a call's failures on; `reject` raises one,
with every field it declares:

```jadpo
use lending/entities { Tool, ToolNotFound }
use lending/types { ToolStatus, ToolView }
use lending/failures { ToolAlreadyLent }
use lending/queries { tool_by_id }

/// Records that the owner has lent a tool out.
action lend_tool(tool_id: Tool.id) fails ToolNotFound, ToolAlreadyLent -> ToolView {
    var tool: Tool = attempt Tool.get(tool_id)
    if (tool.status == ToolStatus.lent) {
        reject ToolAlreadyLent {
            tool_id: tool.id
        }
    }
    var lent: Tool = Tool.update(tool) {
        set: {
            status: ToolStatus.lent
            due_back: clock.now + config.loan_length
        }
    }
    return attempt tool_by_id(lent.id)
}
```

To handle a failure instead of passing it on, `match` on the call:
`success(value) -> ...`, and one arm per failure that recovers, `reject`s
another, or `propagate`s it. Calls are bare in their own domain; another
domain's `public` actions are called through it, `billing.charge(...)`.
No outside call may sit inside a `for`.

## Routes

A route group is a thin door: method, path, input, output and the one
action or query it runs. `GET` and `DELETE` read their input from the
query string, the other methods from the JSON body. `output:` must be
exactly what the route runs returns. A failure's kind sets the status.

```jadpo
use lending/entities { Tool }
use lending/types { ToolView }
use lending/actions { AddTool, add_tool, lend_tool }
use lending/queries { available_tools, tool_by_id }

/// The street's tools.
routes Tools {
    prefix: /tools

    /// Tools free to borrow.
    GET / {
        output: List<ToolView>
        run: available_tools()
    }

    /// Adds a tool the caller owns.
    POST / {
        input: AddTool
        output: ToolView
        status: SuccessStatus.created
        run: add_tool(input, current_principal.user)
    }

    /// One tool.
    GET /{tool_id: Tool.id} {
        output: ToolView
        run: tool_by_id(tool_id)
    }

    /// Lends a tool out.
    POST /{tool_id: Tool.id}/lend {
        output: ToolView
        run: lend_tool(tool_id)
    }
}
```

The group's name matches its file and its prefix. `jadpo check`
writes an OpenAPI document per API to `build/openapi/`. Run
`jadpo api update` after changing a route and commit
`api/main.jadpo_record`: it is the wire contract, and `jadpo check` fails
while it disagrees with the routes.

## Signing in and permissions

`app.jadpo` holds the `application`, whose `jadpo:` is the Jadpo release
line the app is written for, and the `principal`: who a signed-in caller can be.
A user is found by a unique field of an entity. `authentication.jadpo`
lists the ways to sign in.

```jadpo
use lending/entities { Member }

application {
    jadpo: "0.1"
}

principal {
    user: {
        subject: Member.email
    }
}
```

```jadpo
authentication {
    browser_session: {
        cookie: "tool_share_session"
    }
}
```

This keeps people signed in with a cookie, but gives them no way to sign
in yet. `sign_in: { magic_link: { link: "/sign-in" } }` emails a link and
a six-digit code that sign the person in, and needs the session's
`origin:` and an email connection; the [Learn path](learn/learn-sign-in.md)
builds it step by step. There are no passwords. Passkeys go beside the
magic link, and `api_token`, `service_key` and `external_token` sign in
other programs. Code never sees a cookie or a token. Every route needs a signed-in caller unless it says
`authentication: RouteAuthentication.none`; without one it answers 401. A
route reads the caller as `current_principal` and passes
`current_principal.user` to its action.

Policy is always on. Each entity's `policy:` lists who may `create`,
`read`, `update` and `delete` it, and anything not listed is refused. Who
is one of: a user field on the record (`owner_id`), a membership role, `Access.authenticated`,
`Access.public` or `Access.system` for background work. The compiler adds
policy to the SQL of every query and write, so no query filters by owner
by hand. A record the caller may not read is the code's own not-found
failure; a write they may not make is 403 `not_permitted`. A call with no
caller, as tests and fixtures make, runs without policy.

## Configuration

Every setting is a field of the one `config { }` in `config.jadpo`. Each
reads the environment variable named after it in capitals, `LOAN_LENGTH`,
and is checked before the app starts. A field without a `default:` is
required; `test:` gives its value in tests. Code reads `config.loan_length`
anywhere. Secrets are typed (`PostgresUrl`, `SigningKey`) and set locally
with `jadpo config set`.

```jadpo
use lending/types { LoanLength }

config {
    /// How long a tool is lent for.
    loan_length: LoanLength {
        default: 14d
    }
}
```

```jadpo
/// How long one loan lasts.
type LoanLength = Duration {
    min: 1d
    max: 60d
}
```

A fixed value that is the same in every deployment is a `constant`, not a
setting.

## Tests

Tests sit beside their subject as `<name>.test.jadpo` and see everything it
declares. A test is a sentence and a body with `assert`. The kind comes from
what it calls: a function makes a pure test, an action an action test, and
`request { }` a route test. Each test gets its own database, built from the
migrations, and its own clock. `Principal.user(record)` makes a caller, in
tests only.

```jadpo
use lending/entities { Member }
use lending/types { ToolStatus, ToolView }
use lending/failures { ToolAlreadyLent }
use lending/actions { AddTool, add_tool, join }

test "a lent tool is due back in two weeks" {
    var ann: Member = attempt join("ann@example.test")
    var drill: ToolView = add_tool(
        AddTool {
            name: "Drill"
        },
        Principal.user(ann)
    )
    var lent: ToolView = attempt lend_tool(drill.id)
    assert lent.status == ToolStatus.lent
    assert lent.due_back == clock.now + 14d
}

test "a tool can't be lent twice" {
    var ann: Member = attempt join("ann@example.test")
    var ladder: ToolView = add_tool(
        AddTool {
            name: "Ladder"
        },
        Principal.user(ann)
    )
    attempt lend_tool(ladder.id)
    assert lend_tool(ladder.id) fails ToolAlreadyLent
}
```

A route test goes through the real HTTP boundary: input decoding, sign-in,
policy and statuses.

```jadpo
use lending/entities { Member }
use lending/types { ToolView }
use lending/actions { AddTool, add_tool, join }

test "a signed-in member adds a tool" {
    var ann: Member = attempt join("ann@example.test")
    var response: Response = request {
        method: HttpMethod.post
        path: "/tools"
        as: Principal.user(ann)
        input: AddTool {
            name: "Hedge trimmer"
        }
    }
    assert response.status == 201
}

test "only a tool's owner lends it out" {
    var ann: Member = attempt join("ann@example.test")
    var bob: Member = attempt join("bob@example.test")
    var saw: ToolView = add_tool(
        AddTool {
            name: "Saw"
        },
        Principal.user(ann)
    )
    var response: Response = request {
        method: HttpMethod.post
        path: "/tools/{tool_id}/lend"
        params: {
            tool_id: saw.id
        }
        as: Principal.user(bob)
    }
    assert response.status == 403
    assert response.code == "not_permitted"
}
```

Shared test data goes in a `fixture` in `fixtures/`. A test's `fakes:`
scripts what a provider package answers.

## Run it

Copy each file above to its path in an empty folder, all but the API
record, which a command writes. Then make the migration and the record,
check, and run the tests (they need PostgreSQL installed):

```sh
jadpo migrate new initial
jadpo api update
jadpo check
jadpo test
```

```text
checked: 19 files in 0.1 s
tested: 4 passed, 2 action tests, 2 route tests in 1.0 s
```

`jadpo dev` serves it, with a throwaway database of its own. Every route
needs someone signed in, so until you add a way to sign in, a request
is refused:

```sh
curl http://localhost:3000/tools
```

```text
{"error":{"code":"authentication_required","message":"Sign in first.","request_id":"req_9342ee18ff3ce32e"}}
```

## Finding more

Read only what a task needs.

- **The Learn path** builds a todo app with sign-in a step at a time,
  starting from [Learn Jadpo](learn/learn.md).
- **Topics.** Every section of the reference is a topic with a stable ID,
  such as [`named-queries`](topics/named-queries.md),
  [`the-operations-every-entity-gets`](topics/the-operations-every-entity-gets.md),
  [`grants`](topics/grants.md),
  [`magic-links`](topics/magic-links.md),
  [`migrations`](topics/migrations.md) and
  [`kinds-of-test`](topics/kinds-of-test.md). The IDs never
  change.
- **The index.** The docs are published with `index.json`, one entry per
  page with its `id`, `kind`, `title`, a one-line `summary` and its
  `tokens`. Read it, pick pages by title and summary, and fetch only
  those.
- **Outside services.** Packages, providers, connections, drivers and
  capabilities are explained together in
  [`how-integrations-fit-together`](topics/how-integrations-fit-together.md).
- **Errors.** Each rule has a page named after its ID, such as
  [`failure.kind_required`](errors/failure.kind_required.md).
- **Commands.** `jadpo help <command>`, or `jadpo <command> --help`.
- **Whole apps.** The teaching projects in the repository's `examples/`
  folder build up from a first project to apps with tenants, roles,
  providers, background work and AI. Each passes its tests.
- **The rules.** Where anything disagrees with the reference, the
  reference wins.
- **A bug in Jadpo.** If the compiler crashes, rejects code these docs say
  is valid, accepts code they say it refuses, or a page is wrong, read the
  error's page first. Then cut the problem down to the smallest project
  that shows it and ask your user before filing anything: an issue is
  published under their name. Give them a drafted report, or a link to the
  [issue form](https://github.com/jadpo-dev/jadpo/issues/new?template=bug.yml)
  with `title=` filled in. Put no secrets, settings values or other app
  code in it. After a crash, `~/.cache/jadpo/crashes/` holds a file for the
  report; your user reads it before it is attached. Never post on your own.
  A suspected security hole goes to the private route in
  the repository's `SECURITY.md` (GitHub's "Report a vulnerability"
  under Security), never to a public issue.

## Not built yet

A feature that isn't built is the error `toolchain.not_built_yet` where
you use it. Not built yet, among others: seeds and `jadpo seed`; `jadpo rename`; entities and
caches in Redis; failure logging; sign-in with OAuth providers, and OAuth
grants; token scopes; most of personal data, and of the debugging and
review tools; data builders and `assert emitted` in tests; flags, audit,
search and documents; and installers, so `jadpo` is built from its
source with `cargo build --release -p jadpo-cli`.
