Skip to main content

Sign-up

The signUp mutation creates a new identity with an attached person record. It is gated by the LOGIN tenant role — usually the public login token is used to call it.

mutation {
signUp(
email: "[email protected]",
password: "correct horse battery staple",
captchaToken: "0.aXR…"
) {
ok
error {
code
weakPasswordReasons
recommendedAction
developerMessage
}
result {
person { id email }
}
}
}

Inputs

ArgNotes
emailRequired. Validated for format and uniqueness.
passwordPlain-text password. Validated against password policy.
passwordHashOptional alternative to password. Only $2b$ bcrypt hashes are accepted; useful for migrations from existing systems.
rolesOptional global roles to grant. Subject to PERSON_SIGN_UP permission check.
nameOptional display name.
captchaTokenRequired when captcha is configured. (since 2.2)

password and passwordHash are mutually exclusive; pass neither to create a person with no password (intended for passwordless-only or IDP-only accounts).

Errors

CodeCause
INVALID_EMAIL_FORMATEmail failed format validation.
EMAIL_ALREADY_EXISTSAn account with that email already exists. Always returned, regardless of login.revealUserExists — see Enumeration behavior.
TOO_WEAKPassword failed strength checks. See weakPasswordReasons[].
INVALID_CAPTCHACaptcha is configured and the token was missing or rejected. (since 2.2)
RATE_LIMIT_EXCEEDEDPer-IP sign-up rate limit hit. (since 2.2)

recommendedAction (since 2.2)

On EMAIL_ALREADY_EXISTS, the error carries a recommendedAction hint your UI can use to route the visitor to the right next step:

ValueMeaning
SIGN_INThe existing person has a password hash — they should sign in.
RESET_PASSWORDThe existing person has no password (created via IDP or invite without password) — they should go through password reset.

The hint is purely advisory; clients that don't recognize it should fall back to a generic "already registered" message.

{
ok: false,
error: {
code: "EMAIL_ALREADY_EXISTS",
recommendedAction: "SIGN_IN",
developerMessage: "..."
}
}

Enumeration behavior

signUp always returns EMAIL_ALREADY_EXISTS when the address is taken — revealUserExists: false does not suppress it. An earlier silent-success branch (return ok: true, result: null and mail the legitimate owner) was tried and removed: result === null vs result !== null is trivially distinguishable, so the branch did not actually close the enumeration oracle while degrading UX.

For tenants that genuinely cannot tolerate sign-up enumeration the recommended pattern is to gate sign-up behind an invite-only flow (don't expose signUp to the public login token) and run the public flow through createResetPasswordRequest, which is the only auth-flow endpoint that can mask existence without lying about it.

See anti-abuse → enumeration protection for the matrix across all auth flows.

Audit

A person_invite entry is written only for invite / unmanagedInvite. Direct signUp calls are not audit-logged at the tenant level — the resulting person is observable via the regular identity / person queries.