Application web mobile-first pour soirée d'entreprise : authentification passwordless, équipes, buzzer temps réel, galerie photo, sondages avec contraintes métier et classement live.
- Frontend : Next.js 15+ (App Router), React 19, TypeScript
- UI : Tailwind CSS v4 + shadcn/ui (style new-york, base color slate)
- Backend : Supabase (PostgreSQL, Auth OTP, Storage, Realtime)
- Package manager : pnpm
- Hébergement : Vercel
pnpm install
copy .env.local.example .env.local
# éditez .env.local (voir Configuration ci-dessous)
pnpm devL'app est servie sur http://localhost:3000.
- Créez un compte sur https://supabase.com (gratuit jusqu'à 500 Mo / 1 Go storage / 50k MAU).
- Cliquez New project. Notez le mot de passe DB (à conserver pour les migrations).
- Patientez ~2 min pendant l'approvisionnement.
Dans Settings → API :
Project URL→NEXT_PUBLIC_SUPABASE_URLanon public→NEXT_PUBLIC_SUPABASE_ANON_KEYservice_role(secret) →SUPABASE_SERVICE_ROLE_KEY(uniquement nécessaire siAUTH_BYPASS_OTP=true)
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
ALLOWED_EMAIL_DOMAINS=capgemini.com,capgames.local
AUTH_BYPASS_OTP=falseALLOWED_EMAIL_DOMAINS: liste de domaines autorisés à se connecter (séparés par des virgules, sans@).AUTH_BYPASS_OTP: sitrue, le flow d'auth saute la vérification du code 6 chiffres et connecte directement la personne dont l'e-mail est saisi. À RÉSERVER AU DÉVELOPPEMENT.
Ouvrez le SQL Editor du projet Supabase et exécutez dans l'ordre chaque fichier de supabase/migrations/ :
0001_init.sql— schéma (tables, types enum, FK, index)0002_functions_and_triggers.sql— RPC et triggeron_auth_user_created0003_rls_policies.sql— Row Level Security0004_views_realtime_storage.sql— vues, publication Realtime, buckets Storage
⚠️ Le fichier 0004 crée les buckets Storage. Si vous préférez les créer via l'UI Supabase, sautez lesinsert into storage.buckets…mais conservez les policies.
Dans Authentication → Providers → Email :
- ✅ Enable Email provider
- ❌ Confirm email (pas nécessaire pour OTP)
- ✅ Enable Email OTP
Dans Authentication → URL Configuration :
Site URL→http://localhost:3000en dev,https://votre-domaine.vercel.appen prodRedirect URLs→ ajoutezhttp://localhost:3000/**et l'URL de prod
Aucune logique de bootstrap automatique n'est exposée. Connectez-vous une première fois avec votre e-mail dans l'app, puis dans le SQL Editor :
update public.profiles set is_admin = true where email = 'votre.email@domaine.com';Reconnectez-vous et le bouton Admin apparaît. Vous pouvez ensuite promouvoir d'autres utilisateurs depuis /admin/utilisateurs.
src/
├── app/
│ ├── (app)/ ← routes authentifiées (layout dédié)
│ │ ├── page.tsx ← écran d'accueil
│ │ ├── buzzer/ ← liste + salon de buzzer (user)
│ │ ├── photos/ ← galerie photo (user)
│ │ ├── sondages/ ← liste + vote (user)
│ │ ├── classement/ ← leaderboard temps réel
│ │ └── admin/ ← panel admin (équipes, quizz, photos, sondages, points, utilisateurs)
│ ├── auth/callback/ ← callback magic link (bypass OTP)
│ ├── login/ ← page de connexion
│ └── layout.tsx ← layout racine (Toaster)
├── components/
│ ├── app-shell.tsx ← header + bottom nav mobile
│ └── ui/ ← shadcn primitives
├── hooks/
│ ├── use-buzzer-realtime.ts
│ └── use-leaderboard.ts
├── lib/
│ ├── auth.ts ← helpers requireUser / requireAdmin
│ ├── env.ts ← validation env Zod + whitelist domaines
│ ├── utils.ts ← cn()
│ └── supabase/
│ ├── client.ts ← client navigateur
│ ├── server.ts ← client serveur + service role
│ ├── middleware.ts ← refresh session + garde admin
│ └── types.ts ← types DB
├── middleware.ts ← entrypoint Next.js middleware
└── ...
supabase/migrations/ ← scripts SQL à exécuter dans Supabase
Le buzzer doit garantir un ordre équitable malgré la latence réseau variable des téléphones.
Flow :
- L'utilisateur clique sur le buzzer → le client appelle la RPC PostgreSQL
place_buzz(p_room_id). - La RPC s'exécute côté serveur (
SECURITY DEFINER) :- identifie la manche active du salon ;
- insère un buzz dans
public.buzzesavecbuzzed_at = clock_timestamp()(timestamp serveur autoritaire) ; - une contrainte
UNIQUE(round_id, user_id)garantit qu'un même utilisateur ne peut avoir qu'un seul buzz par manche (les clics suivants sont silencieusement ignorés viaON CONFLICT DO NOTHING).
- La table
buzzesest publiée via Supabase Realtime. Tous les clients reçoivent l'INSERT. - Le hook
useBuzzerRealtimereçoit l'événement, refait unSELECT * FROM buzzes_ordered WHERE round_id = ...(vue qui calculerow_number() OVER (ORDER BY buzzed_at)et joint les infos profil / équipe). - L'admin et les joueurs voient la liste mise à jour avec position, nom, équipe et timestamp.
Pourquoi cette stratégie élimine les conflits de concurrence :
- Pas de logique de tri côté client → impossible de tricher en antidatant.
clock_timestamp()(vsnow()) renvoie le moment exact d'exécution de la fonction, et non le moment de début de la transaction → plus précis pour départager des buzzes très proches.- L'index
(round_id, buzzed_at)rend l'ordre déterministe et rapide à requêter. - Le
ON CONFLICT DO NOTHINGévite les re-buzzes parasites.
- Toutes les tables ont RLS activée.
- Les opérations sensibles passent par des RPC
SECURITY DEFINERqui validentauth.uid()et appliquent les règles métier (notammentcast_votequi interdit de voter pour sa propre équipe). - Le helper
public.is_admin()estSECURITY DEFINERetSTABLEpour éviter une récursion infinie dans les policies de la tableprofiles.
| Bucket | Public | Contenu | Écriture |
|---|---|---|---|
team-logos |
✅ | Logos d'équipes | Admin |
photos |
❌ | Photos d'événement | User (préfixe = uid), modération admin |
poll-choices |
✅ | Visuels des choix de sondage | Admin |
Pour les photos privées, l'app génère des URL signées (4h) côté serveur via createSignedUrl.
pnpm dev # serveur dev (Turbopack)
pnpm build # build de production
pnpm start # serveur production (après build)
pnpm lint # eslint- Activez
AUTH_BYPASS_OTP=truelocalement pour itérer sans se faire envoyer un code à chaque connexion. - Créez plusieurs onglets/navigateurs en navigation privée pour simuler plusieurs joueurs.
- Le bouton Admin apparaît dès que
is_admin = truesur le profil.
- Poussez le projet sur GitHub.
- Sur https://vercel.com, Add New → Project et importez le repo.
- Framework détecté automatiquement : Next.js.
- Variables d'environnement à ajouter :
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY(uniquement si vous gardez bypass auth en prod, ce qui n'est PAS recommandé)ALLOWED_EMAIL_DOMAINSAUTH_BYPASS_OTP=false
- Deploy.
- Une fois déployé, ajoutez l'URL de prod dans Supabase → Authentication → URL Configuration → Redirect URLs.
Voir supabase/migrations/0001_init.sql pour les détails. Vue d'ensemble :
auth.users ──1:1── profiles ───N:1── teams
│ │
│ N:1 │ 1:N
└──→ team_email_invites
quiz_rooms ──1:N── rounds ──1:N── buzzes
└──N:1── profiles
photo_albums ──1:N── photos ──N:1── profiles (uploaded_by)
polls ──1:N── poll_choices ──N:1── teams (restricted_team_id)
└──1:N── poll_votes ──N:1── profiles
└──N:1── poll_choices
Vues exposées :
leaderboard: équipes triées par scorebuzzes_ordered: buzzes avec position + infos user/équipepoll_results: agrégat anonyme de votes par choix
- Pas d'historique des points (un simple champ
scoresurteams). Pour audit, ajouter une tablescore_events. - Pas de compression d'image côté client à l'upload — peut saturer le quota Supabase en cas d'usage intensif.
- Pas de PWA (manifest sans icônes ; ajouter des PNGs 192/512 dans
public/si besoin). - Pas de i18n.