Corsia
Preparazione all’esame per conducenti taxi e NCC.
Codice disponibile su richiesta

Il problema
I candidati studiano da circa 3.000 domande ufficiali sparse in vari PDF, più una prova sulla cartina muta. Non c’era modo di esercitarsi nel formato vero dell’esame, che cambia da una sessione all’altra, né di vedere quali argomenti si padroneggiano davvero.
- Stato
- MVP in anticipo, lancio a dicembre
- Stack
- Next.js 16, React 19, TypeScript, Tailwind 4, Supabase, Stripe, Resend, Python, OpenStreetMap
- Facts
- 3.137 domande ufficiali importate
- 13 migrazioni, circa 24 tabelle
- Pipeline dei contenuti da PDF a CSV e mappe SVG
Le scelte che vale la pena spiegare
Le risposte si correggono sul server
Le domande arrivano al browser senza la risposta giusta e con le opzioni mescolate. Una funzione del database corregge ogni risposta, registra il tentativo e limita il tempo dichiarato. L’unica eccezione sono i pacchetti offline, limitati a 200 domande al giorno per dispositivo registrato.
Le regole d’esame sono dati, non codice
Numero di domande, minuti, soglie di superamento e lettere della cartina sono salvati come JSON su ogni sessione d’esame, perché la Regione ha già cambiato formato una volta nel 2026. Un nuovo formato è una riga, non un rilascio.
I webhook di Stripe sono verificati e idempotenti
Ogni evento ha la firma verificata, solo le sessioni pagate danno accesso e i metadati vengono validati. La funzione di acquisto ignora una sessione già vista grazie a una colonna unica, così i tentativi ripetuti di Stripe non danno mai accesso due volte.
Un pezzo del codice
Il browser non conosce la risposta giusta finché non ne ha scelta una.
begin
if auth.uid() is null then
raise exception 'not signed in' using errcode = '42501';
end if;
select * into q from public.questions where id = p_question;
if not found or not private.trainable(q) then
raise exception 'question not available' using errcode = '22023';
end if;
insert into public.attempts (user_id, question_id, chosen_index, correct, ms, mode)
values (auth.uid(), q.id, p_chosen, p_chosen is not distinct from q.correct_index,
least(greatest(p_ms, 0), 3600000), p_mode);
return query select p_chosen is not distinct from q.correct_index, q.correct_index, q.explanation, q.source;
end;