Skip to content

Login With Captcha Gate

Most forms gate submission on field values — email is present, password is filled in, that kind of thing. But sometimes availability depends on state the form doesn’t own. A captcha token comes from an external widget. A permission check lives on the server. A feature flag is set at the session level.

Umpire handles those through conditions: declared inputs that arrive alongside field values at evaluation time. This example shows a login form where submit depends on three separate rules — two field-driven, one condition-driven — and how they aggregate into a single availability result.

LoginCaptcha Gate
🤖 I'm not a robot
reason → Complete the captcha to continue
Complete the captcha to continue
Enter a valid email address
Enter a password
import { check, enabledWhen, umpire } from '@umpire/core'
const loginFields = {
email: { required: true, isEmpty: (value) => !value },
password: { required: true, isEmpty: (value) => !value },
submit: { required: true },
}
type LoginConditions = {
captchaToken: string | null
}
const loginUmp = umpire<typeof loginFields, LoginConditions>({
fields: loginFields,
rules: [
enabledWhen('submit', (_values, conditions) => !!conditions.captchaToken, {
reason: 'Complete the captcha to continue',
}),
enabledWhen('submit', check('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/), {
reason: 'Enter a valid email address',
}),
enabledWhen('submit', ({ password }) => !!password, {
reason: 'Enter a password',
}),
],
})
const result = loginUmp.check(
{ email: 'user@example.com', password: 'hunter2' },
{ captchaToken: null },
)
result.submit
// {
// enabled: false,
// required: false,
// reason: 'Complete the captcha to continue',
// reasons: ['Complete the captcha to continue'],
// }

The field is disabled, so required is suppressed even though the field definition says required: true.

const result = loginUmp.check(
{ email: 'user@example.com', password: 'hunter2' },
{ captchaToken: 'cf-turnstile-xxxx' },
)

Now submit is enabled. The external gate has been satisfied and the field checks pass.

const result = loginUmp.check(
{ email: 'not-an-email', password: '' },
{ captchaToken: 'cf-turnstile-xxxx' },
)
result.submit
// {
// enabled: false,
// required: false,
// reason: 'Enter a valid email address',
// reasons: ['Enter a valid email address', 'Enter a password'],
// }

Two rules fail:

  • the email regex bridge via check()
  • the password presence predicate

Because the email rule was declared first, its message becomes the primary reason.

The captcha token is not a field value the form owns. It comes from an external system, so it belongs in conditions.

That same pattern applies to:

  • feature flags
  • plan tiers
  • abuse cooldowns
  • permission checks
  • remote configuration gates