Skip to main content
Phone sign-in follows the magic-code flow with SMS as the delivery channel. The user enters a phone number, Pylon sends a six-digit code, and a successful verification mints a session. Phone numbers are E.164-normalized (+15551234567) before storage or transport.

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

Response in production (Twilio configured, send succeeded):
Response in dev mode OR if SMS send failed (so the user isn’t blocked):
The dev_code field is returned whenever PYLON_DEV_MODE=true or the SMS transport returned an error. A misconfigured Twilio account then does not lock anyone out during early development. In production with a working Twilio config, dev_code is omitted and only the SMS carries the code. Errors: The code is 6 digits, expires in 10 minutes, and is rate-limited per-number to prevent SMS spam.

Verifying

Response on success:
displayName is optional. Pylon uses it when it creates a new User row because no existing row has this phone. On later 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:
When all three are set, /phone/send-code sends 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

The pylon-auth crate exposes an SmsSender trait. It is the crate-level extension point for a non-Twilio provider (MessageBird, Vonage, Plivo, AWS SNS, or an internal SMS gateway):
The shipped pylon binary wires only the built-in Twilio transport. The /phone/send-code route handler calls it directly via TwilioSmsTransport::from_env(). The prebuilt binary has no env-var switch or registration hook to swap in a custom SmsSender. (PhoneCodeStore only generates and persists the 6-digit code. It returns the code to the caller, which delivers it; it does not hold an SmsSender.) To run a different provider today, 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 phone before storage. (555) 123-4567, 555-123-4567, and +15551234567 collapse to the same canonical form. No two accounts share a number.
  • 6-digit code, 10-minute TTL — same as magic codes.
  • Code burns after wrong attemptstry_verify increments an attempt counter; too many wrong attempts and the code is invalidated server-side. Status 429 INVALID_CODE.
  • Constant-time code comparison — no timing leak on verify.
  • Per-number rate limit on send-code so a phone-number enumeration attack cannot exhaust the SMS budget.
  • Optional CAPTCHA gate — set PYLON_CAPTCHA_PROVIDER to 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 are never written to logs.

Where to go next