Week 030 — OTP End to End
By Thursday, a user could sign in or sign up with a phone number or email, receive a code, verify it, and arrive at their items page.
Week of June 9, 2025
Progress
OTP authentication was completed across both channels. SMS via Twilio was working from the previous week. Email OTP required setting up a Twilio Verify integration with email enabled and DNS records pointing the sending domain to Twilio — the records were entered backwards initially, causing a delay.
Country codes were added to phone number handling — Twilio requires the full international format. The backend, classes, and database were updated for this, followed by sign-in page logic and user profile logic. New users are now registered with a country code.
Sessions are established after OTP verification rather than after the old password-based auth. The setupSession() function was moved out of the OTP verify API and into the common utilities file — there had been two local implementations of it in two different places, which had been causing intermittent session issues.
Went kayaking on Thursday. No code.
The Hard Part
A design tension emerged early in the week around adding new OTP fields to the DTO structure. The disciplined approach: add the fields to UserDTO, create copy functions for each one, update UserService and UserRepository to carry them through. The impatient reaction: these are just three fields, why does this require touching five places?
The answer is that the DTO pattern's value is exactly that consistency — changes are confined to known places, callers don't need to know how the data is stored. But it's genuinely harder to write, and when you're tired and close to shipping something, that matters.
The Friday discovery: disabled form fields are not included when FormData is constructed from a form. That's the HTML spec — disabled inputs are explicitly excluded. The application had been using disabled fields to show "verified" status (phone number confirmed by OTP, now read-only). Those values were disappearing on submission. Fix: enable the fields before constructing the FormData object, then disable them again after.
Learnings
Disabled form inputs are excluded from FormData per the HTML specification. readonly doesn't have the same behavior — readonly inputs are included. If a field needs to be submitted but not editable, readonly is the right attribute. disabled means "not part of this submission."
Next Week
OTP is done. The user flows need to be mapped and assessed for completion. The error and messaging experience is weak — users get very little useful feedback when something goes wrong.
