The Tenant API exposes a single per-tenant configuration object covering password policy, login behavior, passwordless authentication, captcha, and rate limits. It is read via the configuration query and written via the configure mutation.
Every successful configure call is recorded in the audit log as a tenant_config_change event, with captcha.secret redacted to ***.
Reading configuration
query {
configuration {
signup {
requireEmailVerification
}
emailChange {
requireVerification
}
password {
minLength
requireUppercase
requireLowercase
requireDigit
requireSpecial
pattern
checkBlacklist
checkHibp
}
login {
revealUserExists
revealLoginMethod
baseBackoff
maxBackoff
attemptWindow
defaultTokenExpiration
maxTokenExpiration
}
passwordless {
enabled
url
expiration
}
captcha {
provider
threshold
protect { signUp passwordReset passwordlessInit emailVerification }
}
rateLimits {
signUpPerIp { limit window }
loginPerIp { limit window }
passwordResetPerIp { limit window }
passwordlessInitPerIp { limit window }
emailVerificationPerIp { limit window }
}
}
}
The captcha.secret is intentionally not exposed by the schema — it is a credential, write-only.
Writing configuration
mutation {
configure(config: {
password: { minLength: 12, checkHibp: true, checkBlacklist: true },
login: { revealUserExists: false, defaultTokenExpiration: "PT30M" },
captcha: { provider: turnstile, secret: "0x4AAA…" },
rateLimits: {
signUpPerIp: { limit: 5, window: "PT1H" },
loginPerIp: { limit: 20, window: "PT1H" },
passwordResetPerIp: { limit: 5, window: "PT1H" },
passwordlessInitPerIp: { limit: 5, window: "PT1H" },
emailVerificationPerIp: { limit: 5, window: "PT1H" }
}
}) {
ok
error { code developerMessage }
}
}
All ConfigInput fields are optional and partial — unset fields preserve the current value.
Requires the CONFIGURE tenant permission. By default this is held by SUPER_ADMIN.
Prefer keeping configuration in version control? Describe it in a typed tenant.config.ts and apply it with the CLI — see declarative configuration.
Sections
signup (since 2.2)
| Field | Default | Notes |
|---|---|---|
requireEmailVerification | false | New accounts must verify their e-mail before they can sign in. Captured per account at sign-up; toggling affects only accounts created afterwards. See e-mail verification. |
emailChange (since 2.2)
| Field | Default | Notes |
|---|---|---|
requireVerification | false | Self-service changeMyProfile e-mail changes go through confirmEmailChange against a token mailed to the new address, instead of swapping immediately. Independent of signup.requireEmailVerification. See e-mail verification. |
password
See password policy for individual fields and the WeakPasswordReason enum returned with TOO_WEAK errors.
| Field | Default | Notes |
|---|---|---|
minLength | 8 | |
requireUppercase, requireLowercase, requireDigit, requireSpecial | 0 | Minimum count of each character class |
pattern | null | Optional regex; pattern violations yield INVALID_PATTERN |
checkBlacklist | true | 10k-most-common list with leetspeak normalization |
checkHibp | false | HIBP k-anonymity check; opt-in. See anti-abuse. Available since 2.2. |
login
| Field | Default | Notes |
|---|---|---|
revealUserExists | true | When false, unknown-email failures on sign-in / reset / passwordless-init are masked. See anti-abuse — enumeration protection. |
revealLoginMethod | true | (since 2.2) When false, signIn collapses NO_PASSWORD_SET / INVALID_PASSWORD into a generic INVALID_CREDENTIALS and signUp omits the recommendedAction hint on EMAIL_ALREADY_EXISTS. Orthogonal to revealUserExists — UNKNOWN_EMAIL is still controlled by that flag. |
baseBackoff | PT1S | Starting backoff between per-email login attempts. Also drives the per-email mail-init throttle for password reset and passwordless init. |
maxBackoff | PT1M | Upper bound for the exponential backoff. |
attemptWindow | PT5M | How long failed attempts are remembered. |
defaultTokenExpiration | PT30M | Session token lifetime when the client omits expiration. |
maxTokenExpiration | 6 months | Hard cap on client-requested expiration. |
Intervals follow ISO 8601 duration syntax (PT5M, PT1H, P1D, …).
passwordless
See the passwordless page.
captcha (since 2.2)
See anti-abuse — captcha.
| Field | Notes |
|---|---|
provider | turnstile, hcaptcha, or recaptchaV3. null disables captcha. |
secret | Write-only. The provider's server-side secret. Encrypted at rest with the tenant's Providers keychain. Passing null leaves the stored value unchanged; passing "" clears it. |
threshold | reCAPTCHA v3 score threshold (0.0–1.0). Ignored for hCaptcha / Turnstile. |
protect | Per-flow enforcement. The provider/secret is shared; these flags pick which mutations require a token. signUp / passwordReset / passwordlessInit default true; emailVerification defaults false (opt in if requestEmailVerification is publicly exposed). Has no effect while provider is null. |
rateLimits (since 2.2)
Sliding-window per-IP limits. limit: 0 (the default) disables that scope. See anti-abuse — rate limits.
| Scope | Applied to |
|---|---|
signUpPerIp | signUp |
loginPerIp | signIn, signInIDP, signInPasswordless |
passwordResetPerIp | createResetPasswordRequest |
passwordlessInitPerIp | initSignInPasswordless |
emailVerificationPerIp | requestEmailVerification |
Recommended hardening baseline
mutation {
configure(config: {
password: { minLength: 12, checkBlacklist: true, checkHibp: true },
login: { revealUserExists: false, revealLoginMethod: false },
captcha: { provider: turnstile, secret: "…" },
rateLimits: {
signUpPerIp: { limit: 5, window: "PT1H" },
loginPerIp: { limit: 20, window: "PT1H" },
passwordResetPerIp: { limit: 5, window: "PT1H" },
passwordlessInitPerIp: { limit: 5, window: "PT1H" },
emailVerificationPerIp: { limit: 5, window: "PT1H" }
}
}) { ok error { code developerMessage } }
}
For most tenants this is enough to make brute-force, password-spray, and enumeration attacks impractical without affecting legitimate users.