Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferenceSign-in and identity

APIsSign-in and identity

A person the app has not seen

The first time someone signs in whom your app has no user for, Jadpo asks your app what to do. on_first_sign_in: in authentication { } names the action that decides: create a user, link one that already exists, or refuse. One action decides for every way in.

It runs when:

  • a magic link or code is used for an address no user has;
  • a provider returns an account not yet linked to a user;
  • an external token names a subject not yet linked to a user.

Here the action refuses an address Google hasn't verified, and creates a member otherwise:

domains/accounts/actions/create_member.jadpo
// domains/accounts/actions/create_member.jadpoaction create_member(first: FirstSignIn) fails SignUpClosed -> Member.id {    if (first.method == SignInMethod.google and not first.address_verified) {        reject SignUpClosed    }    var member: Member = attempt Member.create(...)    return member.id}
FirstSignIn field Type Holds
method SignInMethod The way in: magic_link, each provider named under sign_in:, or external_token; a built-in enum with one value per way the app declares
address Email? For a magic link, the address, verified; for a provider, its email claim; for an external token, none
address_verified Bool true for a magic link; the provider's own verified claim for a provider; false for an external token
subject Text? The provider's or token's subject; none for a magic link
name Text? The display name a provider's profile gives, when it gives one
  • The action takes exactly one FirstSignIn and returns the id of the principal's user entity: a user it created, or one that already exists. It runs in its own transaction before the session starts.
  • To refuse, it fails with any failure it declares. The person always sees the same built-in sign_in_refused, whatever the reason. The log and the SignInRefused event keep the real one.
  • For a provider or an external token, Jadpo then records the link, from the method and subject to the user's id, in the jadpo_sign_in_links system table, so the next sign-in skips the action. A magic link needs no link: Jadpo finds the user by address:.
  • Linking a user who already exists, by their address, is the action's choice. Do it only when address_verified is true, so no one can take over an account by claiming its address at a provider that doesn't check it.
  • Without on_first_sign_in:, only users who already exist sign in. An unknown address gets no email, and an unknown provider account or token subject is sign_in_refused.
  • A passkey never runs it. Only a person already signed in can add a passkey, so every passkey that signs in has its user.

An external token is signed in on every request, so its first sign-in happens on the first request it comes with:

  • Jadpo looks for the user whose subject: field holds the token's subject, then for one a first sign-in linked it to, in one read. A subject that isn't of the field's type is looked for among the links only.
  • The action runs once for each subject. Requests that arrive together with a new subject, as a page's first load sends them, wait for the first, then sign in as the user it gave.
  • The user the action gives is linked even when they aren't active:, and is refused with user_disabled until they are. So an action can take someone on to wait for approval without being asked again.

Here the staff API takes the company provider's tokens, as in External tokens in tests and jadpo dev, and someone's first token takes them on as a librarian who waits for a manager's approval:

example
use deskauthentication {    browser_session: {        cookie: "desk_session"    }    external_token: {        issuer: config.issuer        audience: config.audience    }    on_first_sign_in: desk.take_on_newcomer}
example
use desk/entities { Librarian }use desk/failures { SubjectTaken }/// Takes on someone the company's provider vouches for, to wait for a/// manager's approval.action take_on_newcomer(first: FirstSignIn) fails SubjectTaken -> Librarian.id {    match first.subject {        none -> {            reject SubjectTaken        }        some(subject) -> {            var newcomer: Librarian = attempt Librarian.create {                set: {                    subject: subject                    name: "New starter"                    active: false                }                conflict: {                    Librarian.subject: SubjectTaken                }            }            return newcomer.id        }    }}

Markdown for agents: /docs/a-person-the-app-has-not-seen.md.Built from Jadpo main at aee2e8f.