Pistoia, luglio 2026
C’è un bug che mi ha fatto perdere un sabato intero. Il cron job che ricalcola le date dei mercatini ogni mattina sembrava funzionare — i log erano puliti, nessun errore — ma i mercatini mostravano date passate. Trecentosessantotto mercatini con la data dell’ultima edizione invece di quella successiva. Li ho trovati per caso, guardando la pagina di un mercato che sapevo essere mensile.
Questo è il tipo di problema che non trovi finché non guardi nel posto giusto. E per trovare il posto giusto ci vuole tempo — un tempo che non si misura in ore ma in tentativi.
Vintagery è nato da una ricerca su Google. Cercavo mercatini vintage vicino a Pistoia per un sabato di novembre e ho trovato: una pagina Facebook del 2018, un blog con le foto sfocate di un mercatino a Lucca, un sito del comune con un PDF da scaricare. Niente che ti dicesse quando si fa, se c’è ancora, se è al coperto.
Ho pensato che qualcuno lo avrebbe fatto prima o poi. Ho aspettato qualche mese. Poi ho smesso di aspettare.
La prima versione era una tabella. Letteralmente: un foglio di calcolo con i mercatini che conoscevo io, aggiornato a mano. Poi è diventata una landing page con un campo email. Poi ha avuto un database. Poi le pagine per regione e città. Poi le email del venerdì, poi le push notification, poi i pagamenti, poi la dashboard per i negozi, poi quella per gli organizzatori.
Non ho mai avuto un piano d’insieme. Ho costruito quello di cui si aveva bisogno nel momento in cui serviva.
Il problema più lungo non è stato scrivere il codice. È stato scoprire che il codice funzionava ma non come pensavo.
La service role key di Supabase non è disponibile in produzione su Vercel. Non è documentato bene da nessuna parte — o almeno non nell’ordine in cui lo cerchi quando non capisci perché la funzione che funziona in locale restituisce un errore silenzioso in produzione. Ho passato un pomeriggio a controllare log, riscrivere endpoint, controllare variabili d’ambiente, prima di capire che il problema era strutturale: certe operazioni devono essere riscritte con row-level security e il client normale, non con il client admin che non esiste in prod.
Cose del genere non si risolvono velocemente. Si risolvono nel momento in cui capisci il modello. E il modello lo capisci solo dopo aver sbagliato abbastanza volte.
Il hardening della sicurezza è stata la settimana più lunga. Dodici vulnerabilità, alcune banali, alcune meno. La più sottile era nel moderatore di immagini: controllavo se l’URL iniziava con il dominio corretto invece di fare un check sull’hostname dopo aver parsato l’URL. La differenza tra startsWith('https://storage.supabase.co') e new URL(imageUrl).hostname === 'storage.supabase.co' sembra piccola. Non lo è — la prima si bypassa con un sottodominio costruito ad hoc.
Non avevo mai pensato a questo tipo di attacco prima di sedermi a fare una security review seria. Non è che sia impossibile da sapere — è che finché non la cerchi non la vedi. E spesso non la cerchi perché hai altro da fare.
La dashboard analytics mostrava i numeri delle visualizzazioni per ogni mercatino. Sembravano giusti — piccoli, ma plausibili. Per qualche mese non ho avuto motivo di dubitarne.
PostgREST — il layer che Supabase usa per esporre il database via REST — restituisce al massimo 1000 righe per query, per impostazione predefinita, silenziosamente. Se fetchi le visualizzazioni di pagina di un mercatino e poi conti con .length, ottieni al massimo 1000. Anche se ce ne sono diecimila. Per un mercatino con poco traffico non cambia niente. Per uno più visitato il contatore si fermava a 1000 e non avanzava più, senza nessun avviso.
La fix è una riga: select('*', { count: 'exact', head: true }). Restituisce il conteggio reale senza scaricare nessuna riga. Non sapevo esistesse finché non ho capito cosa stava sbagliando.
Le push notification su Safari non funzionavano. Il codice sembrava corretto, i log erano puliti, ma su iPhone non arrivava niente. Ho trovato il problema dopo qualche ora: la funzione che convertiva la chiave VAPID da base64 restituiva un tipo sbagliato — un ArrayBuffer invece di un Uint8Array. Safari li tratta diversamente. Un tipo sbagliato, zero errori visibili, comportamento silenziosamente rotto.
Questi sono i bug che ricordo. Non i più grandi — i più silenziosi.
C’è una cosa che ho imparato su Vintagery: non fidarsi mai dei log puliti. I log puliti significano che non stai guardando nel posto giusto.
Il deploy su Vercel sembrava funzionare ma il dominio non si aggiornava. Il comando sbagliato deployava sul dominio Vercel, non sull’alias personalizzato. Lo script che uso ora è tre righe in bash. Ci sono arrivato dopo aver deployato due volte una fix urgente senza che arrivasse in produzione.
I dati dei mercatini vengono da fonti che non seguono nessuno schema. Siti di comuni aggiornati nel 2017, pagine di associazioni culturali locali, PDF scannerizzati. Ho scritto uno scraper in Python che consulta queste fonti per trovare nuovi mercatini e aggiornamenti alle date. Qualsiasi regex che scrivessi per estrarre nome, indirizzo, cadenza e mesi attivi smetteva di funzionare sulla fonte successiva. Ho messo Claude Haiku nel mezzo: gli passo il testo grezzo, lui restituisce JSON strutturato. Non è magia — è che un modello linguistico gestisce la variazione testuale in modo che nessuna regex può fare.
Ho trovato 8 profili utente senza username. Il trigger del database che doveva generarne uno automaticamente alla registrazione non funzionava per tutti i flussi di autenticazione. Il problema era che il controllo per decidere se mandare l’utente all’onboarding era sull’username, non sul campo onboarding_done. Google OAuth precompilava un username dall’email. Quindi il sistema pensava che l’onboarding fosse completo quando non lo era, e quegli utenti erano entrati nell’app con un profilo vuoto.
Ho corretto il trigger, aggiunto il check su onboarding_done, e stavo per andare avanti quando ho pensato: quante altre assunzioni simili ho nel codice? Ho guardato. Ne ho trovate altre due.
Non so esattamente quante ore ci ho messo. Non le ho contate. So che ci sono weekend interi che non ricordo cos’altro ho fatto. So che alcune sere ho smesso di lavorarci perché non riuscivo più a vedere chiaramente il problema, e la mattina dopo era ovvio.
Il progetto è live, funziona, ha cose dentro che non mi aspettavo di costruire quando ho iniziato. Ha anche cose che non ho ancora costruito e che vorrei. Ha fragilità che conosco e che non ho ancora sistemato. Ha decisioni prese in fretta che terrei e decisioni prese con cura che rivedrei.
È così che si finisce con un progetto personale — non quando è finito, ma quando ci convivi abbastanza a lungo da conoscerne i difetti come i tuoi.
Alberto
