Skip to main content
Same shape as magic-code email sign-in, just with SMS as the delivery channel. The user enters a phone number, Pylon sends a 6-digit code, the user types it back, Pylon mints a session. Phone numbers are E.164-normalized (+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

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 — 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

Response on success:
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:
When all three are set, /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

The pylon-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):
Note the shipped 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 phone before storage. (555) 123-4567, 555-123-4567, and +15551234567 collapse 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 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 can’t tie up 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 itself are never written to logs.

Where to go next