Tech stack

What the coded app actually runs on, read from the code.
A deliberately light system: a no-framework JavaScript web app on Cloudflare Pages, a Node / Express + SQLite API on Fly.io, browser-native WebRTC for the calls, and Stripe destination charges for the 80/20 split. Every third-party sits behind an adapter that runs as a deterministic mock until its key exists, so the whole product works end-to-end today and goes live by adding keys, no code change.
Read from the app + API package.json, Dockerfile, and Fly config · 07/24/2026 · 19/19 API tests passing.

Web app

pay-phone.pages.dev · fan, creator, and operator in one page
Vanilla JavaScript
One hash-routed single page (app.js), no framework, no build step
Shared design system
styles.css + admin.css, the same system as the mockups site
Dark + light
Follows the device, remembers the toggle per user
Operator console
#/admin, bookmark-only and sign-in gated; zero traces in the consumer app
Cloudflare Pages
Hosting + single-page fallback (_redirects)

Live calls

Browser-native, peer-to-peer
WebRTC
RTCPeerConnection 1:1 video in the browser; no vendor SDK, no per-minute fees
getUserMedia
Camera + mic capture, permission-gated
WebSocket signaling
/rtc on the API pairs the two sides of a booking
Peer-to-peer media
Call video flows device-to-device, never through our servers
Full-screen call UI
FaceTime-style: remote video never cropped, mirrored self view

Backend API

pay-phone-api.fly.dev · three dependencies total
Node22
Slim Docker image
Express5
HTTP API
SQLite
better-sqlite3 on a Fly volume: bookings, users, sessions, the ledger
ws
WebSocket server for call signaling
Fly.iosjc
Always-on machine, health checks, persistent data volume

Auth & identity

Passwordless, hashed at rest
Magic links
Sign-in email via Resend; works once, expires in 60 minutes
Opaque sessions
Random tokens, only the SHA-256 hash stored, 30-day expiry
Bearer middleware
Ownership checks + rate limiting on every authed route
Phone OTP
Twilio adapter for code sign-in, stubbed until keys

Money

Stripe · 80/20 · no clawbacks
Authorize at booking
PaymentIntent with manual capture: money held, not moved
Capture on completion
Destination charge: 80% routes to the creator, our 20% is the application fee in the same capture
Connect Express
Hosted onboarding collects the creator's debit card + identity check
Instant payout
Unlocks with the confirmed-creator flag; standard payout for everyone else

Go-live switches

Every vendor mocks until its key exists (adapters.js)
Stripe
Real SDK path behind STRIPE_SECRET_KEY; deterministic mock otherwise
Resend
Live email with the key; dev mode returns the link and sends nothing
Twilio
OTP + SMS drop blasts, stubbed until keys
LiveKit
Reserved for recording egress to R2 when keys exist
Status endpoint
One call reports which adapters are live vs mocked

Share cards

The link is the storefront
Handle URLs
/will, /dre, one short link per creator
Edge middleware
Crawlers don't run JavaScript, so a Pages Function injects per-creator preview text server-side
Live pricing
"LIVE NOW — jump on for $25" with the brand card, cached 60s at the edge

Toolchain & QA

Ship + prove
Native appExpo SDK 57
React Native app on the same API, runs on phone and web
Deploys
wrangler pages deploy for the web, flyctl deploy for the API
API tests19 pass
Built-in node --test, including truth-layer checks that recompute the 80/20 money math

How a booking travels

  • Book: the fan taps a slot on the creator's page; the Express API writes the booking to SQLite and Stripe authorizes the card with manual capture: money held, not moved.
  • Sign in: passwordless: a Resend magic link that works once; the session lives as a hashed token with a 30-day expiry.
  • Ring: at showtime both sides open the call page; WebSocket signaling pairs them and the video connects peer-to-peer over WebRTC; it never touches our servers.
  • Complete: hang up → Stripe captures; the 80/20 split happens inside the same capture, and confirmed creators get instant payout.
  • Share: the creator's link unfurls anywhere with live pricing, injected at the Cloudflare edge because crawlers don't run JavaScript.
Guiding principle: light and mock-until-live. Three npm packages on the server, zero on the web app; every vendor sits behind an adapter that mocks until its key exists, so the whole product runs end-to-end today and goes live by adding keys, no code change.