I'm Gabriel Luis Pacheco, founder of BataTalk — a Brazilian product for spoken English practice with AI characters and an in-scene coach (Tato). This is a build post, not a growth flex.
BataTalk helps Brazilians who understand English but freeze when it's their turn to speak. Practice happens by voice in the browser: AI characters in everyday scenes (café, airport, interview), and Tato cues the start of the sentence when you blank. Levels A1–C2. Free plan: one guided conversation per day, no card. Paid plans are priced in BRL on /planos. Honest limits: AI can err; not a certificate; not a live teacher; no accent score theater.
Node 24+, vanilla JS for the app shell, marketing pages SSR'd as HTML, Supabase Auth + Postgres (service role on the API, RLS as second layer), Stripe, Resend, Cloudflare Turnstile, Render + Cloudflare. Voice path: browser mic → our API → transcription / model reply. We intentionally do not store the audio file.
The target user will not "just prompt ChatGPT every day." So the product ships scenes with goals and a coach for the freeze moment. That kills tempting features: social feed, streak theater, fake certificates, pronunciation grades based on flaky ASR text.
Browser-first matters in Brazil: cold start on mobile Chrome during a work break beats "download our app first."
"First line without signup" is great UX and catnip for bots. We gate with Turnstile + daily caps + health checks on transcription. When any dependency is down, the client must fail with a specific reason. Mapping "browser has no SpeechRecognition" to the same bucket as "trial off" poisoned our analytics and made us chase the wrong incident.
Lesson: error taxonomy is product infrastructure. If you collapse distinct failures into one string, you will "fix" the wrong thing for a week.
Password minimum length, email confirmation, Turnstile, IP rate limits. Funnel math is brutal: many client-side signup_started events versus few completed accounts. Instrument failure reasons before you simplify auth into oblivion. Also: mobile layout that pushes the CTA below the fold is a silent killer — we shipped a CSS order fix after measuring absurd scroll height on small screens.
If marketing content only exists after JS hydration, you lose search and LLM citation. We keep substantive copy in SSR HTML for landings, FAQ, and compare pages, and we publish /llms.txt with stable facts (what it is, what it is not, pricing page link, privacy posture). robots.txt allows major AI crawlers on purpose — GEO needs third-party mentions too, but blocking bots while asking to be cited is self-owning.
Voice is not a free client-side feature. Quotas (daily + rolling 30-day) and plan gates are core product. The free tier teaches the loop; it is not infinite GPT with your brand on top. Day boundary at 21:00 America/Sao_Paulo surprised early testers who thought "midnight" — document clock skew in the UI, not only in the terms.
Shipping many thin intent landings (interview ×3, airport ×2) diluted authority. Consolidation with 301s toward one strong URL per intent is boring editorial work and better SEO than another 400-word page.
Site: https://batatalk.com.br — first spoken line can work without an account. If you're building similar voice UX, I'm especially interested in how you handle guest quotas without training a fraud dataset on your credit card, and how you explain mic permission inside in-app browsers (Instagram/TikTok WebViews are hostile). Comments welcome — especially dissenting UX takes from people who teach English for a living.
Support tickets often start with "my free conversation disappeared." Nine times out of ten it is the Brasília day boundary, a Turnstile failure inside a WebView, or the user opening the site through Instagram's in-app browser where the mic never appears. Write those three cases into the UI empty-states. Your future self will spend fewer nights re-explaining the same incident.
If you ship voice in production, budget for: transcription spend, captcha fails, and the emotional cost of a demo that works on your laptop and dies on a mid-range Android Chrome tab with three other media tabs open.