Flashie.
Spaced-repetition flashcard PWA shipped across app, landing, and API subdomains, backed by a self-hosted Convex backend and Postgres.

Flashie is a spaced-repetition flashcard PWA built on a fully self-hosted Convex backend running over Postgres. It ships as three subdomains, app, landing, and API, with reactive queries driving the SM-2 review scheduler, a timezone-aware stats engine, and an installable offline-capable client. The whole stack deploys from a single git tag through a Docker and GitHub Actions pipeline.
The backend is Convex run in self-hosted mode against Postgres 16, using the official convex-backend image rather than the managed cloud. The schema models users, decks, cards, stars, and a full review log, with carefully chosen compound indexes. A by_user_due index over (userId, dueAt) lets the due-card queries fetch only overdue cards in index order, so the most-overdue cards surface first without scanning the table. Authentication is Convex Auth with username and password, wired through HTTP routes and JWT keys generated at setup time.
Scheduling is a faithful SM-2 implementation in reviews.ts. Each review recomputes the ease factor with the classic SM-2 formula, clamps it to a 1.3 floor, resets the repetition streak on a failed grade, and advances the interval through the 1, 6, then interval times ease progression. Every review is persisted with its before and after interval and ease factor, which gives the app a complete, auditable history rather than only the card's current state.
The stats query turns that review log into a study dashboard entirely server-side. It buckets reviews into local days using a client-supplied timezone offset, then derives current and longest streaks, a roughly 17-week contribution heatmap, pass accuracy, mastered-card counts at the 21-day maturity threshold, and a forward due forecast that rolls all overdue cards into today. The day-index math is done with integer arithmetic so the buckets stay stable across DST and timezones.
The frontend is React 18, Vite, and Tailwind, shipped as a PWA via vite-plugin-pwa with an autoUpdating service worker and a standalone install manifest. A Vite alias maps #convex to the backend's convex directory so the client imports Convex's generated, fully typed API directly, giving end-to-end type safety across the wire. Delivery is automated: pushing a v* tag deploys the Convex functions, builds a multi-stage Docker image with VITE_CONVEX_URL baked in, pushes it to GHCR, and SSH-deploys it behind nginx with immutable asset caching and SPA fallback.