Le tre fonti di verità
Le credenziali (database, JWT, segreto HMAC con la platform) stanno nel
.env del server e nel Vault. Mai nel repo, mai in questa documentazione.
L’architettura non si sposta
Il tool è diviso in tre pezzi che vivono in posti diversi: backend e frontend sul droplet DigitalOcean, database su Supabase, più un progetto Vercel che builda il frontend ma non serve il sito pubblico. Questa divisione fa venire la tentazione di «rifare il backend da un’altra parte» (su Vercel, su Supabase Edge, su un nuovo server) o di attaccare un secondo backend allo stesso database. Non si fa. Gli studenti e Cosmica usano il tool ogni mese: un secondo backend, o un database parallelo, li rompe in silenzio (dati scritti in due posti, tenant che divergono, riconciliazioni che spariscono). Finché una decisione esplicita di Mattone non dice altro:- il backend è uno, gira sul droplet, si aggiorna solo da
maindi questo repo; - il database è uno, quello canonico su Supabase, e nessun altro servizio ci scrive sopra;
- il frontend pubblico è quello servito dal droplet: il build Vercel serve a verificare che compili, non a servire gli studenti;
- chi vuole cambiare questa architettura apre una discussione prima, con piano di migrazione e rollback, non un branch.
Perché esistono queste regole
Fino ad agosto 2026 la produzione non corrispondeva amain: c’era codice che viveva solo sul server, e un git reset --hard avrebbe cancellato roba viva. Sullo stesso server girava anche un servizio che col tool non c’entrava niente. Il tentativo di aprile di spostare il database su Supabase era fallito perché le tabelle avevano la protezione per riga accesa e l’utente dell’app non le vedeva: il tool sembrava vuoto ed è stato riportato indietro. Ogni regola qui sotto chiude uno di questi buchi. Il resoconto degli incidenti sta nel repo interno.
I guardrail nel codice
db-guard.ts: all’avvio confronta l’host del database con quello canonico. Se è diverso, o è un host DigitalOcean, il processo esce. Non ci sono eccezioni self-service: se un caso lo richiedesse davvero, si concorda prima con il team e la procedura è nel repo interno.scripts/preflight-deploy.sh: prima del deploy verifica albero git pulito,HEADpresente suorigin, nessun certificato DigitalOcean,tsc --noEmita zero errori. Se fallisce, non si deploya./api/health: risponde condb.canonical, un’impronta dell’host eschemaGuard. Se nella risposta manca la chiavedb, in produzione gira codice vecchio.assertTenantId: ogni query dei repository lancia un errore se il tenant manca. Motivo: TypeORM ignoratenantId: undefinede restituirebbe i dati di tutti i tenant.- Script da riga di comando: richiedono
--tenant=<id>oTENANT_ID, nessun default. Un audit esiste:node backend/scripts/audit-tenant-scope.js. - Migration all’avvio spente: le migration non partono all’avvio; si applicano a mano via SQL dopo averle lette.
Come si fa una modifica
1
Branch e PR sul repo
Lavori in locale su un branch, apri una pull request verso
main. Niente commit diretti su main, niente modifiche sul server.2
Typecheck e test in locale
npm run typecheck nel backend deve dare zero errori. Il preflight lo ricontrolla.3
Merge
La PR si mergia su
main. Il frontend viene buildato anche da Vercel (utile per vedere che compila), ma il sito pubblico è servito dal server.4
Deploy sul server
Sul droplet, nella cartella del progetto:
git fetch, git checkout main, git pull --ff-only, poi ./scripts/preflight-deploy.sh. Solo se è verde: docker-compose -f docker-compose.prod.yml build backend && docker-compose -f docker-compose.prod.yml up -d backend. Per il frontend lo stesso con frontend.5
Verifica
La prova buona è
curl -s https://reconciliation.mattone.co/api/health: deve rispondere db.canonical: true e schemaGuard: "ok". Infine login dalla platform e un conteggio noto (es. i movimenti di un mese del tenant Cosmica).Da non fare mai
- Mai modificare file sul server e lasciarli lì. Se una cosa funziona solo sul droplet, non esiste.
- Mai
git reset --hardsul server: prima si controllagit status, se c’è roba non committata si porta su GitHub. - Mai far partire il backend su un database diverso da quello canonico. Se un caso lo richiede, si concorda prima con il team e si scrive nella PR.
- Mai
DB_MIGRATIONS_RUN=true. Le migration si leggono e si applicano a mano. - Mai una query o uno script senza tenant.
- Mai un database di sviluppo sul server di produzione, e mai una porta database esposta su internet.
- Il deploy si fa solo a mano, con la procedura qui sopra. La vecchia GitHub Action di deploy è ferma: prima di anche solo prendere in considerazione di riaccenderla va riscritta e rivista.
- Mai fidarsi della documentazione sullo stato del server: la verità è
curl /api/healthedocker exec ... printenv DB_HOST.
Cose note da sistemare
- L’elenco puntuale dei residui da ripulire nel codice sta nel repo interno, insieme a chi li ha in carico.
- Alcuni documenti in
docs/del repo descrivono endpoint o regole di abbinamento diversi dal codice. Fa fede il codice; questa pagina è allineata al 6 settembre 2026. - Nella configurazione di avvio le migration automatiche sono spente, ma alcune note nel repo dicono ancora il contrario.
Stato al 6 settembre 2026 — passaggio completato e consolidato. Il server gira
main sul database canonico su Supabase. Verificato allo switch: preflight verde e conteggi delle tabelle principali uguali a quelli del database precedente. Il giorno dopo sono state riattivate le regole di accesso per riga su tutte le tabelle — con verifica che l’applicazione continuasse a funzionare — e corrette le date invertite rimaste dall’importazione, confrontate una per una con il backup.Il database precedente non si legge e non si scrive: è fuori dal giro, e la sua dismissione è una procedura programmata del team. Sia il piano di rientro sia quello di dismissione stanno nel repo interno con le loro verifiche — non sono operazioni self-service.Da qui in poi vale la regola in cima: main è la produzione, e la verità sullo stato del server è curl /api/health, non questa pagina.