Buyer Auth
- Buyer enters phone number (or email) on their portal’s login page
POST /api/auth/otp/sendnormalizes the identifier, validates with Turnstile, applies rate limits, and triggers OTP delivery- Buyer enters the 6-digit code →
POST /api/auth/otp/verify - If the buyer doesn’t exist yet: a new
buyersrecord is created (self-service registration) - Lucia session cookie (
convertella_session) is set - Buyer is redirected to their dashboard or saved redirect target
Delivery Channels
Section titled “Delivery Channels”| Channel | Implementation | Notes |
|---|---|---|
src/lib/otp/cf-email.ts via Cloudflare Email Service | Primary delivery method | |
src/lib/otp/cf-email.ts → WhatsApp fallback | Optional, per-agent configuration |
Key Differences from CRM OTP
Section titled “Key Differences from CRM OTP”| Aspect | Buyer OTP | CRM OTP |
|---|---|---|
| Service | src/lib/otp/service.ts (uses stored procs + TenantContext) | src/lib/auth/admin-otp/service.ts (direct Drizzle, no tenant) |
| Registration | Self-service (OTP verify creates buyer) | Invite-only |
| Delivery channels | Email + WhatsApp | Email only |
| Rate limiting | Stored-proc (IP + identifier, persisted) | IP rate limit (in-memory) |
Code Organization
Section titled “Code Organization”| File | Purpose |
|---|---|
src/lib/otp/service.ts | sendOtp() and verifyOtp() — wraps stored procedures |
src/lib/otp/deliver.ts | deliverOtp() — channel dispatch |
src/lib/otp/cf-email.ts | sendOtpEmail() — Cloudflare Email integration |
src/lib/otp/hash.ts | generateOtpDigits(), hashOtp() (HMAC-SHA256) |
src/lib/otp/normalize.ts | normalizeIdentifier() — phone/email normalization |
src/lib/otp/ip-rate-limit.ts | IP-based rate limiter (in-memory rolling window) |
src/lib/otp/constants.ts | OTP_EXPIRY_MS, OTP_MAX_ATTEMPTS |
src/lib/otp/errors.ts | OtpError class and error codes |
src/lib/otp/client-ip.ts | IP extraction from request headers |