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).

E-mail verification (since 2.2)

When signup.requireEmailVerification is enabled, signUp still succeeds but Contember mails a verification token to the address and the account cannot sign in (signIn returns EMAIL_NOT_VERIFIED) until it is verified. The requirement is frozen onto the account at sign-up time, so toggling the flag never affects existing accounts. See e-mail verification.

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.