Il momento esatto in cui ti rendi conto
Sono le 22:47 di un martedì. Hai appena fatto deploy della prima versione del sito che hai promesso al cliente. In localhost tutto funzionava: il login, il form contatti, il salvataggio dei prodotti. Apri il dominio in produzione, clicchi "accedi", inserisci email e password, premi invio.
Niente.
Il browser si ferma per un secondo, poi una scritta rossa appare in console che non avevi mai visto prima. Il cliente ti scrive: "Funziona?". Tu rispondi "sì sì, qualche test finale".
Apri Claude e scrivi: "il login non funziona in produzione, aiutami". Otto messaggi dopo, dopo aver provato cinque modifiche al codice che peggiorano la situazione, ti chiedi se non sia il caso di chiamare un amico programmatore vero. Avevi dimenticato qualcosa di banale.
Questo articolo ti spiega cosa, perché, e — soprattutto — come riconoscerlo in cinque minuti la prossima volta.
Cosa pensa il vibecoder (sbagliando)
"Se in localhost funziona, in produzione deve funzionare. È lo stesso codice."
Errato. Il codice è lo stesso, ma il contesto in cui gira è completamente diverso. Localhost e produzione sono due ambienti che si somigliano ma non sono identici, e ogni differenza tra i due è un punto in cui qualcosa può rompersi.
Pensa a una macchina. La porti dal meccanico, la prova in officina, frena bene, accelera bene. Tu la porti a casa e ti accorgi che il navigatore non funziona. Cosa è cambiato? Non la macchina. È cambiato il GPS, perché la spia del GPS è collegata al satellite, non al meccanico. Il tuo sito ha decine di "satelliti" invisibili: il dominio, l'HTTPS, le variabili d'ambiente, il database remoto, il CORS, le porte del firewall.
Cosa succede davvero (localhost vs produzione)
Quando il tuo sito è in localhost (http://localhost:3000):
- Il server gira sulla tua macchina, sulla porta 3000
- Frontend e backend stanno sullo stesso indirizzo (entrambi
localhost:3000) - Le variabili d'ambiente le legge dal tuo
.env.local - Il database è un container Docker o un'istanza Supabase di sviluppo
- HTTPS non c'è — viaggi in HTTP semplice
Quando il sito è in produzione (es. https://miosito.com):
- Il server gira su una macchina remota (VPS Aruba, Vercel, Railway), porta interna 3000 ma esposta come 443 (HTTPS)
- Frontend e backend potrebbero essere su domini diversi (es.
miosito.comeapi.miosito.com) - Le variabili d'ambiente le legge dal pannello del provider, non dal tuo
.env - Il database è un'istanza separata, raggiungibile via stringa di connessione configurata
- HTTPS è obbligatorio, i cookie devono avere flag
Secure
Già qui hai sei differenze. La maggior parte dei "non funziona in produzione" è una di queste sei.
I 5 motivi per cui il tuo sito si è rotto (in ordine di frequenza)
1. Le variabili d'ambiente non sono settate
Il tuo .env.local ha 12 righe. OPENAI_API_KEY=sk-..., DATABASE_URL=postgres://..., NEXTAUTH_SECRET=.... In localhost le legge automaticamente. In produzione, su Vercel/Netlify/Aruba, devi inserirle manualmente nel pannello del servizio.
Sintomo: l'app si avvia, la home si vede, ma appena tocchi una funzione che usa quella variabile (es. login, fetch da API esterna, query DB) ricevi un 500 senza spiegazione.
Diagnosi rapida: apri i log del server. Cerca righe tipo undefined is not a function o Cannot read property 'X' of undefined o connection refused. Se vedi qualcosa che si chiama proprio come una tua variabile, hai trovato il colpevole.
Soluzione: pannello del provider → "Environment Variables" → aggiungi la riga mancante → redeploy. Su Vercel basta un click. Su Aruba VPS è un edit del .env.production e un pm2 restart.
2. CORS
Il tuo frontend è su https://miosito.com e il tuo backend è su https://api.miosito.com. Il browser, per sicurezza, vieta al primo di parlare col secondo se il secondo non lo autorizza esplicitamente. È il CORS (Cross-Origin Resource Sharing).
In localhost non succede mai perché frontend e backend stanno entrambi su localhost:3000. In produzione, appena il dominio cambia, il browser chiede al backend "ehi, autorizzi anche miosito.com a parlarti?". Se il backend non risponde con Access-Control-Allow-Origin, il browser blocca la richiesta.
Sintomo: errore esplicito "CORS policy" in console del browser (non nei log del server).
Soluzione: nel backend devi configurare CORS per accettare richieste da https://miosito.com. Approfondimento dedicato qui. Mai usare * in produzione: lascia entrare chiunque, è un buco di sicurezza.
3. La build non è quella che pensi
Hai modificato un file alle 22:30. git push alle 22:35. Sono le 22:50 e il sito mostra ancora la vecchia versione.
Vercel/Netlify ricostruiscono ad ogni push. Ma su VPS Aruba con PM2 devi farlo a mano: pnpm build e pm2 restart. Se non lo fai, il server gira ancora con la build precedente.
Sintomo: hai modificato qualcosa di evidente (testo di un bottone) e il sito mostra la vecchia versione. Hard reload (Ctrl+Shift+R), niente.
Soluzione:
git pull && pnpm build && pm2 restart <progetto>
Se anche dopo il browser mostra la vecchia versione, è cache CDN/browser: hard reload, oppure ?v=2 come query param di test.
4. Cookie e HTTPS si tradiscono a vicenda
Il login funzionava in localhost. In produzione l'utente fa login, viene reindirizzato alla dashboard, e... viene rimandato al login. Loop infinito.
Quasi sempre è la flag Secure del cookie di sessione. In produzione il sito è HTTPS, il cookie viene impostato come Secure. Se per qualche motivo c'è un redirect su HTTP, il cookie non viene inviato e il server pensa che l'utente non sia loggato.
Soluzione: forzare HTTPS dovunque. Verificare che il dominio del cookie sia corretto. Approfondimento su cookie e sessioni qui.
5. Il database remoto non è lo stesso del database locale
In localhost hai un Postgres che gira sul tuo PC. Hai 12 utenti di test, hai aggiunto la colonna phone_number ieri.
In produzione il database è un'altra cosa: un'istanza Supabase, o un Postgres su Neon. Quel database non ha la colonna phone_number se non hai fatto la migration.
Sintomo: errore 500 con messaggio column "phone_number" does not exist.
Soluzione: applica le migration in produzione. Drizzle: pnpm drizzle-kit push. Prisma: prisma migrate deploy. Approfondimento backup & migration qui.
📘 Vuoi tutto il framework completo?
Nel libro Vibecoding Serio trovi questi 5 motivi spiegati con infografica della "checklist pre-deploy", più un'appendice di 15 punti da spuntare prima di mandare in produzione. €14,90 PDF, aggiornamenti gratuiti a vita.
Compra il libro — €14,90→Il framework "Diagnostica in 5 minuti"
Quando il sito si rompe in produzione, segui questo ordine. Non saltare passi.
- Minuto 1 — Apri la console del browser (F12). Network tab. Ricarica. Cerca richieste in rosso. Annota lo status code: 401, 403, 404, 500, CORS error.
- Minuto 2 — Apri i log del server. Vercel: Functions → Logs. Aruba:
pm2 logs <progetto> --lines 50. Copia l'errore completo, non solo il primo paragrafo. - Minuto 3 — Identifica la categoria:
- CORS in console → motivo 2
- 500 + "undefined" su una variabile familiare → motivo 1
- 500 + "column does not exist" → motivo 5
- Login loop → motivo 4
- Sito vecchio nonostante deploy → motivo 3
- Minuto 4 — Leggi la sezione corrispondente.
- Minuto 5 — Applica la soluzione. Una modifica alla volta. Se non funziona, rimettila come prima e prova la seconda.
Come parlarne a Claude per ottenerlo bene
Prompt da NON usare (ti farà perdere 30 minuti):
Claude non ha visibilità sul tuo ambiente, sui log, sul codice. Risponderà con una lista generica di "controlla A, B, C".
Prompt da usare (soluzione in 1 iterazione):
Sintomo: l'utente fa login con successo (vedo 200 OK su /api/auth/callback/credentials nel network tab), viene reindirizzato a /dashboard, ma /dashboard lo rimanda subito a /login. Loop infinito.
In localhost:3000 funziona. Cosa potrebbe essere e in che ordine controllare?
Differenza nel risultato:
- Prompt vago: Claude propone 8 cose generiche, ne provi 4, nessuna funziona.
- Prompt informato: Claude in genere ti chiede una verifica precisa (es. "stampa il valore di
cookies()dentro la dashboard server component") perché sa già che è probabilmente cookie/secure flag su NextAuth v5.
Il pattern è sempre questo: stack dichiarato, sintomo preciso, confronto localhost/produzione, cosa hai già provato. Più contesto dai, meno iterazioni servono.
La regola che ti porti a casa
Localhost mente. Funziona in localhost vuol dire "il codice è probabilmente giusto", non "il sito è pronto per la produzione".
Le cose che si rompono in produzione si rompono per via dell'ambiente, non del codice. Variabili, domini, HTTPS, database remoto, build, CORS. Cinque dei sei sono cose configurate fuori dal tuo IDE, in pannelli di provider o in file .env che non versioni.
Per ognuna delle cinque, ora hai un sintomo, una diagnosi, una soluzione, e un prompt buono per Claude. Tienile a portata di mano la prossima volta — possibilmente prima delle 22:47.