How it works
User row with that email and stamps emailVerified. Typing the code proves control of the address.
Send a code
codeLength is the number of digits in the code (see Code length). Use it to size the code input.
In dev mode (PYLON_DEV_MODE=true), a request from the same machine also gets dev_code in the response, so you can sign in without an email provider configured. Requests from other devices don’t; the code prints to the pylon dev terminal instead.
Verify a code
token and pass it as Authorization: Bearer pylon_a1b2c3... on subsequent requests.
From the SDKs
Built-in security
- Constant-time code comparison — no timing leak.
- 5-attempt cap per code — burned after 5 wrong tries; even a correct subsequent attempt returns
RATE_LIMITED. - 10-minute expiry — codes are short-lived.
- 60-second send cooldown per email — clients can’t flood a user with codes. Returns
429 RATE_LIMITEDwithretry_after_secs. - Single-use — verifying a code consumes it.
- CSPRNG-generated — codes are uniform random over every code of the configured length, zero-padded (
0427is a valid 4-digit code). - Daily wrong-guess limit per email — see Wrong-guess limit.
Code length
PYLON_AUTH_CODE_LENGTH sets the number of digits in every one-time code Pylon sends: magic codes, email verification codes, and phone codes. The value is a whole number from 4 to 8. The default is 6.
3, 9, six) stops the server at boot with an error that names the variable. The send responses return the length as codeLength.
Wrong-guess limit
Each code burns after 5 wrong tries, and a new code can be requested every 60 seconds. Without a limit across codes, an attacker could make thousands of guesses a day against one email. Pylon counts wrong guesses per email (and per phone number) over a rolling 24 hours. When the count reaches the limit, Pylon refuses new codes and verifies for that email until the oldest counted wrong guess is more than 24 hours old. The limit scales with the code length, so an attacker’s daily chance of guessing a code stays near 0.1%:
A correct code clears the count for that email. The count is stored in the auth database (
_pylon_code_failures in SQLite or Postgres), so a restart or deploy does not reset it.
While an email is locked, /magic/send, /magic/verify, /email/send-verification, and /email/verify return:
429. A locked email refuses even the correct code.
Configuring email delivery
Magic codes need an email provider configured, otherwise they only work in dev mode. Pylon supports:Settings → Email for production to keep deliverability high.
If you set PYLON_EMAIL_PROVIDER=webhook, also set PYLON_EMAIL_ENDPOINT to your custom URL. Pylon POSTs { to, from, subject, body } to it.
To give auth email its own key and from-address, separate from your app’s ctx.email, set the PYLON_AUTH_EMAIL_* family (_PROVIDER, _API_KEY, _FROM, _ENDPOINT). It overrides PYLON_EMAIL_* for auth flows only. See Auth → Configuration.
See Stack0, Resend, SendGrid for transactional sending services.
Customizing the email
The default subject is “Your sign-in code” and the body is plain text:PYLON_EMAIL_TEMPLATE_MAGIC_CODE_SUBJECT and PYLON_EMAIL_TEMPLATE_MAGIC_CODE_BODY. The template can use {{code}} and {{app_name}} (from PYLON_EMAIL_APP_NAME). Any other {{...}} is removed.
Error responses
Testing
In dev mode, skip the email entirely:When to choose magic codes vs alternatives
Most apps should start with magic codes, then add OAuth for one-click sign-in once they have users. Password is the third option, useful when email is not available (for example a shared device with no email access).