Inspiration

Traditional flashcard apps make you do all the work: writing every card by hand, guessing when to review, and picking a payment provider that may not even work in your country. We wanted flashcards that adapt to you — an app where AI turns "I need to learn X" into a ready-to-study deck, where the review schedule is driven by real memory science instead of a fixed daily count, and where paying for Pro is as easy with Mercado Pago (for LATAM users) as it is with PayPal (everyone else). Flashi started from a simple frustration: spaced repetition works, but every tool that implements it makes the setup harder than the studying.

What it does

Flashi is an AI-assisted, offline-first flashcard app built on real spaced repetition (FSRS):

Smart scheduling — every card reschedules itself based on how well you remembered it, using the FSRS algorithm (state, stability, difficulty, next-review-date) instead of a naive "review every day" list. AI study assistant — a tool-calling chat agent that creates flashcards from a topic, batch-generates cards into an existing deck, checks how many cards are due, searches the public marketplace, and suggests the best reminder time based on your actual study history. Marketplace — browse, search, and filter public decks by tag, see what's trending, share your own decks via a link/deep-link, and collect decks other people made into your own library. Smart reminders — daily, weekly, one-time, or AI-picked "smart" reminders, delivered via push notification, with streak and due-card tracking. Offline-first — a local SQLite store (Drift) with a bidirectional sync queue, so you can review cards on a plane and have everything reconcile automatically once you're back online. Freemium + subscriptions — a free tier with sensible limits, and Pro (monthly/yearly/lifetime) unlocking unlimited decks/cards, more AI generations, marketplace publishing, and advanced analytics — payable via Mercado Pago or PayPal, both driven through the same in-app WebView checkout screen.

How we built it

The backend is split into two Go services on purpose: flashi-backend (public API, JWT auth, business logic — bcrypt, FSRS scoring, AI orchestration, subscription state machines) and flashi-database (the only process allowed to open a Postgres connection, exposing a narrow internal HTTP API behind a shared-secret header). Every domain — decks, study, market, AI, sync, reminders, subscriptions — follows the same pattern: a thin HTTP client in the backend, a real SQL repository in the database service.

Subscriptions are provider-agnostic by design: POST /v1/subscriptions/checkout picks Mercado Pago's preapproval API or PayPal's Billing Subscriptions API depending on what the user chose, but both return the same shape (checkoutUrl + local subscriptionId), so the Flutter app only ever needs one SubscriptionCheckoutPage WebView, regardless of provider. Webhooks are the single source of truth for entitlement — the WebView return URL only tells Flutter when to stop showing the loading screen; the actual "you're Pro now" decision is made when Mercado Pago's or PayPal's webhook lands, gets verified (signature check on both), deduplicated ((provider, event_id) unique constraint), and mirrored onto users.plan / plan_expires_at.

On the Flutter side: flutter_riverpod for state, go_router for navigation, webview_flutter for checkout, flutter_local_notifications + FCM for reminders, and a custom brutalist design system (BrutColors, BrutType, a variable-weight custom font) for the UI.

Challenges we ran into

Splitting a monolith into a database-owning service without breaking anything. Every domain's repository had to move from direct SQL to HTTP calls without changing external behavior — including transactional operations (FSRS review writes, sync push processing) that couldn't be split across a network hop, so those became single composite endpoints instead of naive CRUD. Two payment providers, one mental model. Mercado Pago and PayPal don't share a status vocabulary (authorized vs ACTIVE, paused vs SUSPENDED...), and lifetime plans needed special handling on both sides (a one-shot preapproval on MP, a single-cycle billing subscription on PayPal) so they'd never expire. Debugging a live production outage layer by layer. After deploying, flashi-database couldn't reach Postgres — turned out to be a moving target: first a wrong port in DATABASE_URL, then a Docker container that had been restarted instead of recreated (so it kept the stale env var even after the .env file was fixed). We added an explicit Ping()-on-boot log line specifically so this class of bug shows up immediately instead of surfacing as a generic 500 three layers up. Finding the bugs that only show up live. Static review wasn't enough — running a real subscribe → webhook → cancel → expire cycle against the actual database surfaced a bug where a merely pending subscription leaked paid-plan feature names into the API response before the user had actually paid.

Accomplishments that we're proud of

A subscriptions system where both Mercado Pago and PayPal are fully wired end-to-end in production — real checkout, real signature-verified webhooks, real entitlement sync — not a sandbox demo. Catching and fixing three real, non-obvious bugs (an MP sandbox-detection typo, a PayPal lifetime period-end leak, and a pending-subscription feature leak) through systematic audits before they hit real users. A clean architectural boundary — one service, one database connection — that made every other feature (decks, AI, sync, reminders) easier to reason about and test in isolation. Shipping all the way to a signed, production-configured release build (APK + AAB) with real subscription credentials, not just "works on my machine."

What we learned

Webhooks-as-source-of-truth is the only sane pattern for payments. Anything that trusts a redirect URL for entitlement is one dropped connection away from giving away free access. docker restart ≠ docker recreate. An env var change silently does nothing until the container is actually recreated — a classic trap that cost real debugging time and taught us to add explicit connectivity logging at boot instead of inferring it from downstream errors. Live testing finds what code review can't. Every bug we shipped a fix for was invisible in the diff and only showed up by actually running the subscribe/cancel/expire cycle against real data. Separating "who talks to the database" from "who has business logic" pays off immediately the first time you need to debug which layer is actually broken.

What's next for Flashi: AI Flashcards

Finish the Play Store listing (privacy policy, screenshots, feature graphic) and submit to iOS. A proper upgrade/downgrade flow — today, changing plans requires cancelling first, which is a rough edge we want to smooth out. A reconciliation job that periodically cross-checks subscription state against each provider, so a missed webhook can never permanently desync entitlement. Richer AI features: image-based card generation, deck recommendations based on what you're struggling with, not just what you searched. Public sharing polish for the marketplace — collections, following creators, leaderboards for study streaks.

Built With

Share this project:

Updates