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

Guides

Ship a change behind a flag

You end with a change deployed switched off, both sides tested, turned on with no deploy, and the flag removed.

On this page

A feature flag lets you merge and deploy a change switched off, then turn it on for some users or for everyone without another deploy. Jadpo knows every read of every flag, so a misspelt flag is a compile error, both sides of each flag must have a test, and removing a flag is one command. This guide puts a shop's new one-page checkout behind a flag, switches it on in a running app, and removes the flag.

You start from an app with a GET /checkout route that runs an action called start_checkout, which answers with how many pages the checkout takes. A line in policy.jadpo lets anyone call it, since it reaches no entity that would say who may. Build an API shows how to write a route.

Declare the flag

Every flag is declared once, in flags.jadpo at the root of the project. default: is what everyone gets until you change it. owner: and expires: make sure a flag doesn't stay for good: past that date jadpo check fails with an error naming the owner.

flags.jadpo:

jadpo
flags {    /// The one-page checkout replacing the three-step form.    new_checkout: Bool {        default: false        owner: "payments"        expires: "2099-01-31"    }}

A flag with more than two values is an enum instead of a Bool, which is how you compare two layouts.

Read it

An action reads the flag as flags.new_checkout. The value stays the same for the whole operation, so one request never sees it change halfway through. You don't pass the caller: Jadpo already knows who is asking.

domains/shop/actions/start_checkout.jadpo, changed:

jadpo
use shop/types { CheckoutShown }/// The checkout this customer is shown.action start_checkout() -> CheckoutShown {    if (flags.new_checkout) {        return CheckoutShown {            page_count: 1        }    }    return CheckoutShown {        page_count: 3    }}

Actions and a route's run: arguments can read a flag. Functions and queries can't, so you pass them its value as a parameter.

Test both sides

jadpo check won't pass until tests reach this read with every value the flag can have, so the path you are about to switch on is known to work:

jadpo check

error No test reaches this read of flags.new_checkout

--> domains/shop/actions/start_checkout.jadpo:5:9

  |5 |     if (flags.new_checkout) {  |         ^^^^^^^^^^^^^^^^^^ not tested with every value
reason
Each read of a flag is tested with every value it can have, so each branch is known to work before the flag is switched. No test reaches it. A test that doesn't name the flag in its flags: gives it the default:, false.
next
Add a test that reaches this read and leaves new_checkout at its default, false (agent)
rule
flag.value_untested
help
https://jadpo.dev/docs/errors/flag.value_untested

A test chooses a value with flags:, and a flag it doesn't name has its default:.

domains/shop/actions/start_checkout.test.jadpo:

jadpo
use shop/types { CheckoutShown }test "the three-step form is shown by default" {    var shown: CheckoutShown = start_checkout()    assert shown.page_count == 3}test "the new checkout takes one page" {    flags: {        new_checkout: true    }    var shown: CheckoutShown = start_checkout()    assert shown.page_count == 1}

A flag's state lives in your app's own database, in tables Jadpo owns. The first flag needs a migration to make them:

shell
jadpo migrate new add_flags
output
  compared the entities with an empty database  System tables    + table  jadpo_flag_changes  system table for flag changes    + table  jadpo_flags         system table for flags    + table  jadpo_housekeeping  system table for housekeepingWrote database/migrations/20261104T1115_add_flags/    snapshot.json  the whole schema at this point    postgres.sql   4 statements: create table, create indexnext: jadpo check  confirms the migration matches the entities
shell
jadpo test
output
tested: 2 passed, 2 action tests

Now you can merge and deploy. Nobody sees the new checkout yet.

Switch it on

With the app running under jadpo dev, the checkout takes three pages:

shell
curl http://localhost:3000/checkout
text
{"page_count":3}

jadpo flags changes the flag in the running app's database, with no deploy:

shell
jadpo flags set new_checkout true
output
Set `new_checkout`    was      false for everyone (the default)    now      true for everyone    logged   ann · role jadpo    applies  within 10 s, on every server

Each server refreshes its flags every few seconds, so ask again a little later:

shell
curl http://localhost:3000/checkout
text
{"page_count":1}

If it goes wrong, one command puts everyone back on the default. Nothing is redeployed, and every change is logged with who made it:

shell
jadpo flags reset new_checkout
output
Reset `new_checkout`: everyone gets its default    was      true for everyone    now      false for everyone (the default)    logged   ann · role jadpo    applies  within 10 s, on every server

jadpo flags finds the database as other operator commands do: a running jadpo dev's, the address its connection's setting gives, or the one --url names. So production's flags are changed with production's address.

Roll it out a little at a time

A flag doesn't have to be all or nothing. --percent gives a value to a share of signed-in users, and --users to the users you name, by their id. A named user comes first, then the share, then everyone else.

shell
jadpo flags set new_checkout --percent 10
output
Set `new_checkout`    was      false for everyone (the default)    now      true for 10% of users; false (the default) for everyone else    logged   ann · role jadpo    applies  within 10 s, on every server

The same user always gets the same answer, and raising 10% to 20% keeps the first 10% in. A caller who isn't signed in gets the everyone value, and so do schedules and subscribers, which have no caller.

jadpo flags list shows where each flag stands:

shell
jadpo flags list
output
  flag          everyone  users  shares    changed     state  new_checkout  false         –  true 10%  2026-11-04  * rolling out1 flag: main  /private/var/folders/c3/bd2p8dtn3vs2wk9ld2fmk4k00000gn/T/jadpo-pg-75891-0/jadpo_f811f521d4_0_dev_main

It marks a flag "ready to remove" once every caller has had one value for seven days.

Show it to a browser

A page that has to know a flag's value can read it from the app. Add client: true to the flag, and Jadpo generates GET /flags, answered for whoever the request signed in as. A flag without client: true never leaves the server.

Remove the flag

Once the change has proved itself, the flag and the old code have to go. Jadpo knows every read, so it does that for you:

shell
jadpo flags remove new_checkout --keep true
output
  domains/shop/actions/start_checkout.jadpo    ~ flags.new_checkout                              1 read, 1 `if` decided by true  domains/shop/actions/start_checkout.test.jadpo    - test "the three-step form is shown by default"  tested a branch that goes    - new_checkout: true                              in test "the new checkout takes one page"  flags.jadpo    - new_checkout                                    flag, the last: `flags { }` stays, keeping its tablesRemoved `new_checkout`    kept   true    tests  1 removed, each tested a branch that wentnext: jadpo test  runs the tests that are left

The if has gone, along with the three-step branch and its test, and the project still checks. Review the diff and commit it as you would any change.

Read more

Feature flags, Flags: declared, read and tested, Flags: changed, listed and removed, jadpo flags and Test settings.

Not built yet

  • Giving a named tenant a fixed value isn't built yet. Name the tenant's users with --users for now.
  • Erasing a user doesn't yet remove their id from jadpo_flags and jadpo_flag_changes, so a user named with --users stays there. Prefer --percent where you can.

Markdown for agents: /guides/guide-ship-a-change-behind-a-flag.md.Built from Jadpo main at 94ab778. Jadpo's CI compiles this guide's code and runs its tests.