Products
MagicCards
A spaced repetition app in production, with web, mobile and its own SSO
- Role
- End-to-end design and development
- Context
- Personal product in production
- Stack
- Next.js 16, TypeScript, Firestore, Vercel, Upstash Redis, Stripe, Gemini, Expo
- Links
- Live product · magic-cards.fr ↗Private source code, demo available on requestArchitecture ↓

Overview
Flashcard app live at magic-cards.fr, built on spaced repetition (7-box Leitner system): 3D card reviews, real-time duels, timed quizzes, friend streaks, a leaderboard, AI deck generation (Gemini) and Anki import. Pro subscription through Stripe, push notifications, and an Expo mobile app sharing the site's 145 API routes. Google sign-in in production, and my own SSO (MySSO, OIDC/PKCE) in dev and staging.
Highlights
- 017-box Leitner system with 3D card reviews
- 02Real-time duels, timed quizzes, friend streaks and a leaderboard
- 03AI deck generation (Gemini) and Anki import
- 04Firestore as the only database: direct reads, writes only through the API
- 05Expo mobile app sharing the web API routes
- 06dev → staging → main pipeline with lint, tests and E2E CI on every PR
Architecture
Under the hood
A single Next.js backend hosted on Vercel serves the web pages and the API routes shared by the site and the Expo app. Firestore is the only database: clients read it directly and in real time, but never write to it themselves. Every write goes through an API route.
- API handlers shared by web and mobile
- 145API handlers shared by web and mobile
- App Router pages, FR by default and /en
- 83App Router pages, FR by default and /en
- database: Cloud Firestore
- 1database: Cloud Firestore
- Vercel crons a day
- 4Vercel crons a day
- Vitest unit test files
- 31Vitest unit test files
- environments: dev, staging, production
- 3environments: dev, staging, production
Who talks to whom
Clients
Web · PWA
Next.js 16 · React 19 · Tailwind 4 · offline via sw.js
App iOS / Android
Expo 54 · React Native 0.81 · Expo Router 6
Portail admin
Basic Auth + HMAC cookie · read-only impersonation
HTTPS · pages + /api · ID token as Bearer
Vercel · Edge
Middleware proxy.ts
Nonce CSP · maintenance · /admin Basic Auth · admin rate limit
Edge Config
Maintenance switch, no redeploy
Serverless functions
Vercel · Node functions
Pages App Router
83 pages · RSC/SSR · next/og
Routes API
145 handlers · withAuth → verifyIdToken · transactions
Vercel Cron ×4
00:00 · 08:00 · 10:00 · 18:00 UTC
Writes with the Admin SDK · verifies tokens · rate limit
Data
Cloud Firestore
Only database · client writes denied · onSnapshot listeners
Firebase Auth · Storage
Custom tokens, Google · avatars and card images, 5 MB
Upstash Redis
Sliding-window rate limit, nothing else
External services
Gemini 2.5 Flash
AI decks, MCQ distractors
Stripe
Checkout, portal, webhook
Resend
Transactional emails fr/en
Web Push · Expo Push
VAPID, exp.host, reminders
Google OAuth · MySSO
Google in prod, MySSO in dev
Sentry
Client, server, edge errors
Vercel Analytics
Speed Insights, no PII
Design principles
01
One database: Firestore
No SQL, no second database. Redis holds no business data.
02
Direct reads, writes through the API
Clients read Firestore through the SDK. Rules deny almost every client write; the API writes with the Admin SDK.
03
One API for two clients
Web and mobile call the same /api/* routes with a Firebase ID token as Bearer.
04
Points and streaks in transactions
Anything that moves points, tokens or streaks runs in a server-side runTransaction.
05
Quizzes graded on the server
Correct MCQ answers stay in private/ documents the client cannot read.
06
Real time without a dedicated server
No Socket.io or Pusher: Firestore listeners push changes, and Vercel functions stay stateless.
Review engine · 7-box Leitner
- Box 1+1 d
- Box 2+2 d
- Box 3+4 d
- Box 4+7 d
- Box 5+14 d
- Box 6+30 d
- Box 7+90 d
- Box > 7Learned
Correct: the card moves up one box. Wrong: back to box 1. Every answer is a server write (POST /api/review/answer); offline, answers wait in a local queue and are sent when the network returns.
Authentication
In production, sign-in is Google only: Firebase popup on the web, server-side code exchange then a custom token returned by deep link on mobile. MySSO, my OpenID Connect provider, is wired in dev and staging (PKCE, JWKS verification with jose). Every door ends at the same Firebase ID token, verified by every API route.
Scheduled jobs
| UTC | Route | Job |
|---|---|---|
| 00:00 | /api/cron/daily | Closes events, resets streaks, hands out shields, downgrades expired Pro plans if a Stripe webhook was lost |
| 08:00 | /api/cron/remind | Web and Expo push to users with cards due |
| 10:00 | /api/cron/daily-quiz | Closes yesterday's quiz, builds today's MCQ in one batched Gemini call |
| 18:00 | /api/cron/streak-reminder | Reminder to players whose streak is at stake |
CI/CD and environments
feature/*
Local emulators
dev
Vercel · Firebase staging
staging
Pre-production
main
Vercel Production · Firebase prod
Every PR runs GitHub Actions CI: lint, Vitest, build and Playwright E2E against the Firebase emulators. No branch skips a stage, hotfixes included. Mobile: EAS Build and EAS Submit. Renovate keeps dependencies current.
Full stack
| Tech | Version | Role |
|---|---|---|
| Next.js (App Router, Turbopack) | 16.3 | RSC/SSR pages, API routes, middleware |
| React | 19 | Web UI |
| TypeScript | 6.0 | Web, API and mobile |
| Tailwind CSS | 4 | Styling, CSS-variable theming |
| Firebase JS SDK | 12 | Auth, Firestore (IndexedDB), Storage |
| Tech | Version | Role |
|---|---|---|
| Expo SDK · Expo Router | 54 · 6 | iOS and Android app |
| React Native | 0.81 | Native runtime |
| expo-notifications | 0.32 | Push via Expo Push API |
| EAS Build · EAS Submit | — | Cloud builds and store submission |
| Tech | Version | Role |
|---|---|---|
| Vercel Functions + Edge | — | API routes and middleware |
| firebase-admin | 13 | Firestore writes, tokens |
| Cloud Firestore | — | Single database, real time, rules |
| Upstash Redis · @upstash/ratelimit | 1.38 · 2.0 | Global rate limiting |
| sql.js · JSZip | 1.14 · 3.10 | Anki import (SQLite in WebAssembly) |
| Tech | Version | Role |
|---|---|---|
| Google Gemini 2.5 Flash | 2.9 | Deck generation, MCQ distractors |
| Stripe | 22 | Subscriptions, store, portal |
| Resend | 6 | Transactional emails |
| Sentry | 10 | Client, server, edge errors |
| Tech | Version | Role |
|---|---|---|
| Vitest | 4.1 | Unit and integration tests |
| Playwright | 1.59 | E2E against emulators |
| Firebase Emulator Suite | 15 | Offline dev and E2E |
| GitHub Actions · Renovate | — | CI, release, dependencies |
Full architecture document ↗Read from the code, dev branch, 28 September 2026
Screens
Hover or use the keyboard to enlarge a screen




Next project MYSSO
MySSO →