Skip to content

Buyer Auth

  1. Buyer enters phone number (or email) on their portal’s login page
  2. POST /api/auth/otp/send normalizes the identifier, validates with Turnstile, applies rate limits, and triggers OTP delivery
  3. Buyer enters the 6-digit code → POST /api/auth/otp/verify
  4. If the buyer doesn’t exist yet: a new buyers record is created (self-service registration)
  5. Lucia session cookie (convertella_session) is set
  6. Buyer is redirected to their dashboard or saved redirect target
ChannelImplementationNotes
Emailsrc/lib/otp/cf-email.ts via Cloudflare Email ServicePrimary delivery method
WhatsAppsrc/lib/otp/cf-email.ts → WhatsApp fallbackOptional, per-agent configuration
AspectBuyer OTPCRM OTP
Servicesrc/lib/otp/service.ts (uses stored procs + TenantContext)src/lib/auth/admin-otp/service.ts (direct Drizzle, no tenant)
RegistrationSelf-service (OTP verify creates buyer)Invite-only
Delivery channelsEmail + WhatsAppEmail only
Rate limitingStored-proc (IP + identifier, persisted)IP rate limit (in-memory)
FilePurpose
src/lib/otp/service.tssendOtp() and verifyOtp() — wraps stored procedures
src/lib/otp/deliver.tsdeliverOtp() — channel dispatch
src/lib/otp/cf-email.tssendOtpEmail() — Cloudflare Email integration
src/lib/otp/hash.tsgenerateOtpDigits(), hashOtp() (HMAC-SHA256)
src/lib/otp/normalize.tsnormalizeIdentifier() — phone/email normalization
src/lib/otp/ip-rate-limit.tsIP-based rate limiter (in-memory rolling window)
src/lib/otp/constants.tsOTP_EXPIRY_MS, OTP_MAX_ATTEMPTS
src/lib/otp/errors.tsOtpError class and error codes
src/lib/otp/client-ip.tsIP extraction from request headers