Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Identity

Identity

The application and revocation

On this page

A person can be disabled, or a session revoked, while they still hold a valid cookie or token. By default, Jadpo looks up the caller's record on every request, so revoking a session or disabling a user takes effect on the very next request. You don't write anything for that. Sessions and tokens are random values Jadpo looks up in the database.

application { } in app.jadpo can change this with one setting, revocation_delay:, and also holds the app's display_name:.

Fewer database reads: revocation_delay:

Use it when looking up the caller's record on every request costs too much, and a short window in which a disabled user can still act is acceptable.

example
application {    revocation_delay: 5m}

With revocation_delay: 5m, Jadpo signs what it learned from the caller's record into their session or token and trusts that for up to five minutes, without reading the database. A user you disable may keep working for up to five minutes. After that, the next request reads the record again.

Three kinds of request always read the record, whatever the delay:

  • a request with a token from an outside identity provider (external_token, see Ways to sign in);
  • a request that sends a service key itself, rather than a token exchanged for one;
  • a request to a route marked authentication: RouteAuthentication.fresh (Routes: signed in by default).

This is the only revocation setting. Leave it out for the strict behaviour.

How a delayed session is renewed

A credential is issued the same way with or without a delay. A session's cookie and an API token are 32 random bytes, and Jadpo keeps only a verifier of them, a one-way digest that can't be turned back into the credential.

The first request with a new credential reads the record. Its response renews the credential with a part Jadpo signs, holding what that read found and when it stops counting: the delay after the read, or the session's end if that is sooner. Every request after that signs in from the signed part alone. When the part expires, the next request reads the record again and renews it the same way.

  • A browser session's renewed cookie comes back in Set-Cookie, which the browser keeps by itself.
  • An API token's comes back in an X-Jadpo-Token response header. Your client keeps it and sends it from then on. A client that ignores it still signs in, but reads the record on every request.
  • A token a service gets from POST /auth/exchange (see Exchanging a service key for a token) is signed when it is given and never renewed. When it expires, the service exchanges its key again.

Signing keys

With a delay, each way of signing in needs a signing_key:, which reads a SigningKey setting from your configuration (Signing keys). New parts are signed with the setting's current value, and parts signed with its _PREVIOUS value still count, so you can rotate a key without signing anyone out.

A signed part the key doesn't open, or one that has expired, is never refused for that. The request reads the record, as it would with no delay.

The app's name: display_name:

display_name: is your app's name as people see it, such as "Notes". Jadpo's own emails name your app by it, so it is required once you switch on magic links or another built-in email.

A whole app with a delay

The settings, the delay, a user and a service, and each way of signing in with its key:

example
config {    browser_origin: Url    session_signing_key: SigningKey}
example
use journal/entities { Member, Partner }use journal/types { MemberStatus, PartnerStatus }application {    revocation_delay: 5m}principal {    user: {        subject: Member.handle        active: Member.status == MemberStatus.active    }    service: {        subject: Partner.name        active: Partner.status == PartnerStatus.active    }}
example
authentication {    browser_session: {        cookie: "notes_session"        signing_key: config.session_signing_key        origin: config.browser_origin    }    service_key: {        signing_key: config.session_signing_key    }}

A partner signs in with its key, which always reads the record, or with the token the exchange gave it for that key, which reads nothing until it expires. A fresh route reads the record either way:

example
use journal/types { PartnerView }use journal/queries { this_partner }/// What a partner reads, signed in with its key or a token the exchange gave.routes Partners {    prefix: /partners    /// Signs in without a read while the token's signed part lasts.    GET /me {        output: PartnerView        run: this_partner(current_principal.service)    }    /// Reads the partner's record now, whatever the token says.    GET /me/fresh {        authentication: RouteAuthentication.fresh        output: PartnerView        run: this_partner(current_principal.service)    }}

The API record, the file jadpo api update writes to list each API's routes (The public API record), lists the exchange Jadpo answers, as jadpo routes and OpenAPI do (Generated routes):

example
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1api main    authentication browser_session    authentication service_keyPOST /auth/exchange    generated exchange    caller service    output ExchangedToken    status 200GET /openapi.json    generated openapi    status 200GET /partners/me    caller service    output PartnerView    status 200    fails partner_not_foundGET /partners/me/fresh    caller service    output PartnerView    status 200    fails partner_not_foundoutput ExchangedToken    expires_at: Instant    token: ServiceTokenoutput PartnerView    name: Text { min_length: 3, max_length: 40 }failure partner_not_found    status 404

Markdown for agents: /docs/the-application-and-revocation.md.Built from Jadpo main at a707efb.