Produits
MagicCards
Une app de répétition espacée en production, avec web, mobile et son propre SSO
- Rôle
- Conception et développement de bout en bout
- Contexte
- Produit personnel en production
- Stack
- Next.js 16, TypeScript, Firestore, Vercel, Upstash Redis, Stripe, Gemini, Expo
- Liens
- Produit en ligne · magic-cards.fr ↗Code source privé, démonstration possible sur demandeArchitecture ↓

Vue d'ensemble
Application de flashcards en production sur magic-cards.fr, fondée sur la répétition espacée (système Leitner à 7 boîtes) : révisions avec cartes 3D, duels en temps réel, quiz chronométrés, séries entre amis, classement, génération de decks par IA (Gemini) et import Anki. Abonnement Pro via Stripe, notifications push, et une application mobile Expo qui partage les 145 routes API du site. Connexion Google en production, et mon propre SSO (MySSO, OIDC/PKCE) en dev et staging.
Points clés
- 01Système Leitner à 7 boîtes et révisions en cartes 3D
- 02Duels en temps réel, quiz chronométrés, séries entre amis et classement
- 03Génération de decks par IA (Gemini) et import Anki
- 04Firestore comme base unique : lecture directe, écriture uniquement par l'API
- 05Application mobile Expo partageant les routes API du web
- 06Chaîne dev → staging → main avec CI lint, tests et E2E à chaque PR
Architecture
Sous le capot
Un seul backend Next.js hébergé sur Vercel sert les pages web et les routes API que le site et l'app Expo partagent. Firestore est l'unique base de données : les clients la lisent en direct et en temps réel, mais n'y écrivent jamais eux-mêmes. Toute écriture passe par une route API.
- handlers API partagés web et mobile
- 145handlers API partagés web et mobile
- pages App Router, FR par défaut et /en
- 83pages App Router, FR par défaut et /en
- base de données : Cloud Firestore
- 1base de données : Cloud Firestore
- crons Vercel par jour
- 4crons Vercel par jour
- fichiers de tests unitaires Vitest
- 31fichiers de tests unitaires Vitest
- environnements : dev, staging, production
- 3environnements : dev, staging, production
Qui parle à qui
Clients
Web · PWA
Next.js 16 · React 19 · Tailwind 4 · hors ligne via sw.js
App iOS / Android
Expo 54 · React Native 0.81 · Expo Router 6
Portail admin
Basic Auth + cookie HMAC · impersonation en lecture seule
HTTPS · pages + /api · ID token en Bearer
Vercel · Edge
Middleware proxy.ts
CSP à nonce · maintenance · Basic Auth /admin · rate limit admin
Edge Config
Interrupteur de maintenance, sans redéploiement
Fonctions serverless
Vercel · Fonctions Node
Pages App Router
83 pages · RSC/SSR · next/og
Routes API
145 handlers · withAuth → verifyIdToken · transactions
Vercel Cron ×4
00 h · 08 h · 10 h · 18 h UTC
Écrit avec l'Admin SDK · vérifie les tokens · rate limit
Données
Cloud Firestore
Seule base · écriture client refusée · listeners onSnapshot
Firebase Auth · Storage
Custom tokens, Google · avatars et images de cartes, 5 Mo
Upstash Redis
Rate limit en fenêtre glissante, rien d'autre
Services externes
Gemini 2.5 Flash
Decks IA, distracteurs QCM
Stripe
Checkout, portail, webhook
Resend
Emails transactionnels fr/en
Web Push · Expo Push
VAPID, exp.host, rappels
Google OAuth · MySSO
Google en prod, MySSO en dev
Sentry
Erreurs client, serveur, edge
Vercel Analytics
Speed Insights, sans PII
Principes de conception
01
Une seule base : Firestore
Pas de SQL, pas de seconde base. Redis ne stocke aucune donnée métier.
02
Lecture directe, écriture par l'API
Les clients lisent Firestore via le SDK. Les règles refusent presque toutes les écritures client ; l'API écrit avec l'Admin SDK.
03
Une API pour deux clients
Le web et l'app mobile appellent les mêmes routes /api/* avec un ID token Firebase en Bearer.
04
Points et séries en transaction
Tout ce qui déplace des points, jetons ou séries tourne dans runTransaction côté serveur.
05
Quiz notés côté serveur
Les bonnes réponses des QCM restent dans des documents private/ que le client ne peut pas lire.
06
Temps réel sans serveur dédié
Pas de Socket.io ni de Pusher : les listeners Firestore poussent les changements, les fonctions Vercel restent sans état.
Moteur de révision · Leitner à 7 boîtes
- Boîte 1+1 j
- Boîte 2+2 j
- Boîte 3+4 j
- Boîte 4+7 j
- Boîte 5+14 j
- Boîte 6+30 j
- Boîte 7+90 j
- Boîte > 7Apprise
Réussite : la carte monte d'une boîte. Échec : retour en boîte 1. Chaque réponse est une écriture serveur (POST /api/review/answer) ; hors ligne, elles attendent dans une file locale et repartent au retour du réseau.
Authentification
En production, la connexion se fait uniquement avec Google : popup Firebase sur le web, échange du code côté serveur puis custom token renvoyé par deep link sur mobile. MySSO, mon fournisseur OpenID Connect, est branché en dev et staging (PKCE, vérification JWKS avec jose). Toutes les portes aboutissent au même ID token Firebase, vérifié par chaque route API.
Tâches planifiées
| UTC | Route | Rôle |
|---|---|---|
| 00:00 | /api/cron/daily | Clôt les events, remet à zéro les séries, distribue les boucliers, rétrograde les Pro expirés si un webhook Stripe s'est perdu |
| 08:00 | /api/cron/remind | Push web et Expo aux utilisateurs qui ont des cartes à réviser |
| 10:00 | /api/cron/daily-quiz | Clôt le quiz de la veille, génère le QCM du jour en un seul appel Gemini groupé |
| 18:00 | /api/cron/streak-reminder | Rappel aux joueurs dont la série est en jeu |
CI/CD et environnements
feature/*
Émulateurs locaux
dev
Vercel · Firebase staging
staging
Pré-production
main
Vercel Production · Firebase prod
Chaque PR passe la CI GitHub Actions : lint, Vitest, build et Playwright E2E contre les émulateurs Firebase. Aucune branche ne saute d'étape, correctifs urgents compris. Mobile : EAS Build et EAS Submit. Renovate tient les dépendances à jour.
Stack complète
| Techno | Version | Rôle |
|---|---|---|
| Next.js (App Router, Turbopack) | 16.3 | Pages RSC/SSR, routes API, middleware |
| React | 19 | Interface web |
| TypeScript | 6.0 | Web, API et mobile |
| Tailwind CSS | 4 | Styles, thème par variables CSS |
| Firebase JS SDK | 12 | Auth, Firestore (IndexedDB), Storage |
| Techno | Version | Rôle |
|---|---|---|
| Expo SDK · Expo Router | 54 · 6 | App iOS et Android |
| React Native | 0.81 | Runtime natif |
| expo-notifications | 0.32 | Push via Expo Push API |
| EAS Build · EAS Submit | — | Builds cloud et publication sur les stores |
| Techno | Version | Rôle |
|---|---|---|
| Vercel Functions + Edge | — | Routes API et middleware |
| firebase-admin | 13 | Écritures Firestore, tokens |
| Cloud Firestore | — | Base unique, temps réel, règles |
| Upstash Redis · @upstash/ratelimit | 1.38 · 2.0 | Rate limiting global |
| sql.js · JSZip | 1.14 · 3.10 | Import Anki (SQLite en WebAssembly) |
| Techno | Version | Rôle |
|---|---|---|
| Google Gemini 2.5 Flash | 2.9 | Génération de decks, distracteurs QCM |
| Stripe | 22 | Abonnements, boutique, portail |
| Resend | 6 | Emails transactionnels |
| Sentry | 10 | Erreurs client, serveur, edge |
| Techno | Version | Rôle |
|---|---|---|
| Vitest | 4.1 | Tests unitaires et d'intégration |
| Playwright | 1.59 | E2E contre les émulateurs |
| Firebase Emulator Suite | 15 | Dev hors ligne et E2E |
| GitHub Actions · Renovate | — | CI, release, dépendances |
Document d'architecture complet ↗Relevé dans le code, branche dev, 28 septembre 2026
Captures
Survolez ou parcourez au clavier pour agrandir une capture




Projet suivant MYSSO
MySSO →