+15551234567) before any storage or transport call.
Endpoints
The user entity needs a
phone field and optionally phoneVerified:
phone should be unique so two accounts can’t claim the same number.
Sending a code
dev_code field is returned whenever PYLON_DEV_MODE=true or the SMS transport returned an error — so a misconfigured Twilio account doesn’t lock anyone out during early development. In production with a working Twilio config, dev_code is omitted and only the SMS itself carries the code.
Errors:
The code is 6 digits, expires in 10 minutes, and is rate-limited per-number to prevent SMS spam.
Verifying
displayName is optional — used when Pylon needs to create a new User row because no existing row has this phone. On subsequent sign-ins for the same number, displayName is ignored (the existing row’s name stays).
Errors:
On first successful verify Pylon stamps
phoneVerified to the current ISO 8601 timestamp.
Twilio transport (built-in)
The default SMS transport is Twilio. Set three env vars and you’re done:/phone/send-code ships an SMS via Twilio’s REST API. When any are missing, sent is false and the dev_code is returned in the response.
Custom SMS providers
Thepylon-auth crate exposes an SmsSender trait — the crate-level extension point for a non-Twilio provider (MessageBird, Vonage, Plivo, AWS SNS, an internal SMS gateway):
pylon binary only wires the built-in Twilio transport, invoked directly from the /phone/send-code route handler via TwilioSmsTransport::from_env() — there is no env-var switch or registration hook to swap in a custom SmsSender in the prebuilt binary. (PhoneCodeStore itself only generates + persists the 6-digit code — it returns the code to the caller, which is responsible for delivery; it does not hold an SmsSender.) To run a different provider today you build on the pylon-auth crate directly in your own Rust binary; env-configurable third-party transports for the shipped binary are not yet available.
Security guarantees
- E.164 normalization on
phonebefore storage.(555) 123-4567,555-123-4567, and+15551234567collapse to the same canonical form — no two accounts for the same number. - 6-digit code, 10-minute TTL — same as magic codes.
- Code burns after wrong attempts —
try_verifyincrements an attempt counter; too many wrong attempts and the code is invalidated server-side. Status429 INVALID_CODE. - Constant-time code comparison — no timing leak on verify.
- Per-number rate limit on
send-codeso a phone-number enumeration attack can’t tie up SMS budget. - Optional CAPTCHA gate — set
PYLON_CAPTCHA_PROVIDERto require a captcha token before send. See CAPTCHA. - Twilio credentials never logged. On transport failure only the provider error is logged at warn level (
[phone] twilio send failed: <error>) — the SMS body and the code itself are never written to logs.
Where to go next
- Magic codes — email equivalent
- CAPTCHA — gate
/phone/send-codeagainst bot networks - Sessions — what
/phone/verifymints