Il primo commit di un mio progetto si chiama "Initial commit" e porta già auth, database, pagamenti, i18n e un pannello di admin. Il secondo, molte volte, si chiama "remove useless things". Quella coppia di commit riassume meglio di qualunque altra cosa come lavoro: non parto da zero, parto clonando il mio template e tagliando.
Dieci progetti così danno un criterio piuttosto affilato su cosa è nucleo e cosa è rumore. Non è un'opinione: sta nella cronologia, e nei punti dove si ripete la stessa cancellazione.
Cosa si guadagna a partire così
Un SaaS di gestione segnalazioni per un'azienda di trasporti è andato da zero alla produzione in tre giorni. Giorno uno: clonare e togliere quello che avanzava. Giorno due: schema delle segnalazioni, upload di immagini, drawer di dettaglio, cache delle liste. Giorno tre: landing. Tutti e tre i giorni sono stati interamente di dominio. Nessuno ha toccato un login.
Un SaaS di generazione di immagini è partito allo stesso modo. Il giorno dell'"Initial commit" c'era già lo schema delle immagini generate, l'helper di S3, branding e tema. Il giorno dopo c'era già la pipeline di generazione. Auth, Postgres con Drizzle, Stripe e i18n erano già dentro.
Quello che risparmi non è scrivere il login. È il mese di piccole decisioni che si trascina dietro: dove vive la sessione, come si protegge una rotta nuova, cosa succede quando scade il cookie, come si dichiarano le lingue.
Cosa resta sempre
- Auth con un'unica porta. Non una funzione
requireSession()che ogni pagina deve ricordarsi di chiamare, ma unlayout.tsxnell'albero privato. Una pagina nuova è protetta da dove la metti, non dal fatto che ti ricordi di copiare il check. - Postgres con un ORM tipizzato e migrazioni versionate. Qui c'è un dettaglio che ci mette un po' a saltar fuori: se gli snapshot di drizzle si desincronizzano,
drizzle-kit generatefa il diff contro un database vuoto e riemette ilCREATE TABLEdi tutto lo schema. Si sistema ricostruendo lo snapshot iniziale finchégeneratenon torna a dire che non ci sono cambiamenti. - i18n dal minuto zero. Metterlo dopo obbliga a toccare ogni stringa del progetto. Metterlo prima costa un pomeriggio.
- Un admin minimo. Elenco utenti e quattro metriche. Niente altro.
Cosa si taglia sempre
Stripe è il caso chiaro. In un'app di timbrature, le rotte checkout, portal e webhook sono state cancellate per intero lo stesso giorno del commit iniziale: 338 righe fuori. In un template di visualizzatore interno, identico. Due progetti diversi, un anno di distanza, la stessa cancellazione.
La conclusione non è stata togliere Stripe dal template, ma metterlo dietro a un flag:
export const USE_STRIPE = process.env.USE_STRIPE === "true";
export async function POST(request: NextRequest) {
if (!USE_STRIPE) {
return NextResponse.json({ error: "Stripe is not enabled" }, { status: 501 });
}
Un progetto senza pagamenti ora mette USE_STRIPE=false e va avanti. Con una sfumatura che è costata scoprirla: il client di Stripe era null as unknown as Stripe quando il flag era spento, quindi al primo uso distratto dava un TypeError opaco invece di dire cosa stava succedendo. Ora è un Proxy che lancia un errore esplicito.
L'altro taglio è stato RLS. Le tabelle sono nate con crudPolicy({ role: authenticatedRole, read: authUid(table.userId) }) di drizzle-orm/neon. Sono state tolte tutte. Il motivo non è che RLS sia cattiva: è che l'app faceva già tutti i suoi accessi tramite una connessione admin dal server, quindi le policy non venivano mai valutate e davano una sensazione di sicurezza che non esisteva. Due livelli di autorizzazione di cui uno solo è vivo sono peggio di uno solo, perché nessuno sa quale comanda.
E poi l'admin sovradimensionato. Il template del team ha accumulato mesi di pannello di amministrazione e gestione utenti, ed è finito in un commit che lo elimina per intero. Lascia in eredità un costo a tutti i figli e quasi nessuno lo usa.
Il costo differito
È questo che nessuno racconta del lavorare su template: un bug nella madre nasce già clonato in tutti i figli.
Il più caro che ho avuto è stato una riga nel root layout: alternates.canonical: BASE_URL. Qualunque pagina che non sovrascrivesse il proprio canonical si autocanonicalizzava alla home e cadeva fuori dall'indice. È rimasto lì per mesi, in ogni clone. La regola che ne ho tirato fuori: l'assenza di canonical è innocua, un canonical sbagliato no. Da allora il layout condiviso non mette il canonical e ogni rotta mette il suo.
Altri due dello stesso tipo:
- Il
.gitignoreignorava solo.env.local. Il primo passo naturale in un clone ècp .env.example .env. Il primo commit si portava via le chiavi. BASE_URLhardcoded al dominio del template. Da lì dipendevanometadataBase, i canonical, l'hreflang, le URL di OG, la sitemap e la validazione di stessa origine del checkout di Stripe. Tutto silenziosamente sbagliato in qualunque clone, e senza un solo errore in console.
Il rilevamento della lingua era acceptLanguage.includes(locale): ignora l'ordine di preferenza del client e indovina per coincidenza di sottostringa, perché "sl-SI" contiene "sl". Moltiplicato per i progetti che hanno ereditato quella riga.
Come impara il template madre
Le correzioni vanno al contrario: dal figlio alla madre. Quando un progetto scopre qualcosa, la correzione sale nel template e il successivo nasce già con quella. Questo funziona solo se la correzione è accompagnata da un test che fallisce se il difetto viene reintrodotto, perché altrimenti il refactor successivo "semplifica" la regola tornando al bug.
L'ultimo grande irrigidimento del mio template è stato una PR da 39 punti: idempotenza del webhook di Stripe con una tabella processed_stripe_events, identità sempre dalla sessione e mai per argomento nelle server action, userId nel WHERE di ogni lettura di feedback, validazione di stessa origine nelle URL di ritorno del checkout. Meno 11.421 righe, più 7.268. Quasi tutto il cancellato erano tutorial che descrivevano una struttura che non esisteva più.
E il progetto successivo è partito con chore: inicializa desde la plantilla endurecida. È tutto qui il punto.
Un template non è un risparmio di digitazione, è un posto dove conservare quello che hai già imparato. Quello che decide se serve non è cosa porta, è la disciplina di caricargli ogni correzione invece di lasciarla nel figlio dove è apparsa.
