Yesterday I shipped the Billing UI and Customer Portal. Today I went deeper into the monetization flow and hit a critical infrastructure issue.
What's shipped:
-Paywall Interceptor (global 402 handler) - when users hit credit limits, a contextual modal appears with 3 upgrade plans (Starter $19, Growth $49, Enterprise $149) + flexible credit purchases
-Smart PaywallModal - context-aware: Free users see upgrade plans + credit input, subscribed users see only credit input + "Manage Subscription" link to Customer Portal
-Real-time credit calculation - flexible input (min 10 credits, $0.49/credit) with live price updates, consistent across BillingPage and PaywallModal
-Auth flow improvements - /register now returns JWT for auto-login, password validation, redirect to dashboard after signup
-Database architecture migration - moved from direct Turso remote connections to local replica with sync (fixed persistent 502/EOF errors).
Key decisions:
Local replica over direct remote: Turso's libsql driver has bugs with persistent HTTP connections (502 Bad Gateway, unexpected EOF). Local replica with sync_url solves this - reads are instant from local file, writes sync to cloud. Trade-off: slightly more complex sync logic, but 100% reliable.
Context-aware paywall: Instead of generic "buy credits" modal, the PaywallModal checks subscription status via useQuery. Free users get upgrade options, subscribed users get credit purchases. Reduces friction and confusion.
No separate success/cancel pages: Stripe redirects to /app/billing?payment=success|cancel. BillingPage reads query param, shows contextual banner (auto-dismiss 5s + manual close), and force-refetches credit/subscription data. Cleaner UX, less code.
What's left:
-End-to-end payment testing (all scenarios: credit purchase, subscription upgrade, downgrade, cancellation)
-Frontend polish and edge cases
-Deploy to production (Render + Turso sync validation)
9 days to launch.