Un portale clienti per uno studio di una sola persona. I clienti seguono il loro progetto, approvano i preventivi, tengono d'occhio le fatture, mi inviano il materiale che chiedo e interrogano un assistente che conosce il loro progetto.
Io gestisco tutto dalla stessa applicazione.
Unico architetto e sviluppatore
Prodotto, backend, frontend, AI, infrastruttura
Settembre 2026
Otto fasi
FastAPI · React
PostgreSQL · pgvector · Tailwind · Hetzner
Ogni numero qui è stato contato dal repository, non stimato. I controlli si dividono in pytest → 71 su un database usa e getta · end-to-end → 136 su un server in esecuzione · jsdom → 76 che montano la vera interfaccia.
controlli automatici, tutti superati
endpoint API su 131 operazioni
tabelle del database in 8 migrazioni
righe di codice applicativo
chiavi di traduzione, in ciascuna delle due lingue
componenti React
tipi di azione sotto audit
template email, localizzati
Ai clienti di uno sviluppatore freelance le informazioni non mancano perché nessuno le ha scritte. Mancano perché sono sparse sui canali che il lavoro si è trovato a usare: il preventivo è un PDF in un thread di email, la fattura è un secondo PDF tre settimane dopo, le foto richieste sono passate da un link di trasferimento ormai scaduto, e la risposta a "come sta andando" vive nella testa dello sviluppatore.
Il costo ricade su entrambi. Il cliente chiede su WhatsApp perché non c'è altro posto dove guardare. Lo sviluppatore risponde ogni settimana alla stessa domanda, a memoria, e ogni risposta è tempo tolto allo sviluppo. Peggio ancora, nulla è dimostrabile: quando un preventivo è stato approvato, da chi, in che data, è un'affermazione, non una registrazione.
Il vincolo che ha dato forma a tutto
Il cliente di uno studio di una sola persona non deve mai avere bisogno di quella persona per sapere a che punto è il suo progetto.
Un secondo vincolo nasce dallo scopo dello strumento. Non è un prodotto SaaS con tenant. È il sistema interno di un singolo sviluppatore, che custodisce il rapporto commerciale con una manciata di aziende svizzere alla volta. Significa che l'isolamento dei dati non è una funzione da pubblicizzare ma un pavimento su cui costruire, e che ogni decisione poteva essere presa per chiarezza anziché per scala.
Ogni rotta che tocca un progetto verifica la proprietà prima di toccare i dati. Non esiste un middleware "controlla il tenant" da dimenticare: il controllo è la prima riga della funzione che risponde alla richiesta. Tre regole reggono l'intero design:
L'accesso ai progetti passa da un'unica funzione, owned_project, che carica il progetto e lo rifiuta se non appartiene all'azienda di chi chiama. Ogni rotta legata a un progetto la chiama. Un id estraneo restituisce 404, mai 403: il portale non conferma che il progetto di un'altra azienda esista. Ventuno test percorrono le rotte dei clienti con gli identificativi di un'altra azienda ed esigono quella risposta.
Gli upload vivono fuori dalla document root con nomi UUID, e il nome originale del file è conservato solo nel database. Ogni download passa da un endpoint autenticato che ricontrolla la proprietà, quindi un URL trapelato non vale nulla e nulla è indicizzabile. Gli upload sono validati per contenuto, non per estensione: un eseguibile rinominato .pdf, o un file Word rinominato .pptx, viene rifiutato leggendo i magic bytes e, per i formati Office, la struttura interna dell'archivio.
Claude Code riporta i progressi dall'interno del repository di un cliente con un token per progetto, salvato come hash e limitato a tre permessi. Con un token valido, l'API admin risponde 401, l'API dei documenti risponde 401 e la fase di un altro progetto risponde 404. Il riferimento pubblico dell'API documenta quegli undici endpoint e nient'altro, quindi condividere il link non espone alcuna superficie.
La funzione che volevo fare meglio di tutte
È nata dallo stesso ragionamento. Emette un token separato, di breve durata e marcato come impersonificazione, e finché è attivo ogni scrittura viene rifiutata. Vedo esattamente ciò che vede un cliente, senza la possibilità di agire al suo posto.
Un solo posto per il progetto, i documenti, i pagamenti e le domande — pensato per chi non è tecnico.
Fasi con stati e date, un changelog scritto come fatti già veri, e una barra di avanzamento calcolata anziché digitata: completato conta uno, in corso conta mezzo, diviso per il numero di fasi. La stessa aritmetica della mia vecchia dashboard statica, perché era la parte che funzionava.
Preventivi, contratti, fatture, proposte di design e file condividono un unico sistema: griglia o elenco, ricerca, miniature, un visualizzatore PDF integrato con navigazione tra le pagine e zoom, rendering Markdown sanificato e cronologia delle versioni. DOCX e PPTX vengono convertiti in PDF per l'anteprima tramite LibreOffice headless, mentre l'originale resta scaricabile.
Approvare un preventivo richiede di digitare il proprio nome e spuntare una casella; il portale salva il nome così come è stato scritto, data e ora, l'indirizzo IP e lo user agent. La presa visione di un contratto fa lo stesso e dice chiaramente, nella finestra stessa, che è una presa visione e non una firma elettronica qualificata. Il PDF firmato torna attraverso il portale e diventa la versione che vedono tutti.
Le richieste di materiale indicano i formati accettati, un limite di dimensione, una scadenza e se basta una risposta scritta. Gli upload sono riprendibili: il browser invia blocchi da 4 MB e il server risponde con i byte che ha già ricevuto, così una connessione caduta riprende invece di ricominciare da capo un set di foto da 200 MB. Chiedere una correzione apre un nuovo giro e conserva quello precedente.
Le fatture riportano importo, data di emissione e scadenza, e il ritardo è calcolato al momento della lettura anziché salvato, così uno stato non può mai essere superato. Il cliente vede a colpo d'occhio cosa è da pagare, pagato e scaduto, con l'IBAN a cui pagare. I progetti a ore mostrano ogni ora registrata con la sua descrizione, per mese, alla tariffa concordata.
Legge i dati di quel progetto: dettagli, fasi, changelog, il testo estratto di ogni documento, materiali, ore e le mie note private. Se gli chiedi un prezzo che esiste in un preventivo, lo cita. Se gli chiedi di inventarne uno, non lo fa.
"Quanto mi costa aggiungere un e-commerce con 200 prodotti? Dammi una cifra."
"Non ho informazioni sui costi per aggiungere un e-commerce al tuo progetto. Ti consiglio di contattare Matteo per avere un preventivo specifico."
Testato, non sperato
Se gli chiedi di garantire una data di consegna, rifiuta e corregge con la vera data prevista dal progetto. Quel comportamento è lo scopo stesso della funzione, ed è testato anziché sperato.
Un assistente vale quanto ciò che gli è permesso vedere, e un progetto accumula più testo di quanto ne stia in un prompt. Il portale estrae il testo di ogni documento una volta sola, al caricamento, e lo salva. Quando arriva una domanda assembla il contesto completo del progetto e lo misura.
Finché il contesto completo del progetto sta nel budget configurato, il modello lo legge tutto. Verificato: il percorso completo ha risposto a partire dall'intero preventivo.
Oltre il budget ripiega sul retrieval su porzioni vettorializzate degli stessi documenti. Verificato riducendo il budget a 200 token: ha risposto CHF 4'800.00 correttamente, usando solo gli estratti recuperati.
La regola dell'isolamento sopravvive a entrambi i percorsi. Le porzioni sono filtrate per id di progetto dentro la query vettoriale, quindi una ricerca per similarità non può attraversare le aziende nemmeno in linea di principio.
Un server, un dominio, niente di esposto che non debba esserlo.
customers-portal.matteocola.com
TLS · HSTS · CSP · serve la SPA e gli asset statici
systemd, 2 worker · solo localhost
+ pgvector · 38 tabelle
fuori dalla web root
Le risposte arrivano in streaming come server-sent events, e un buffer del proxy trasforma una conversazione in un'attesa seguita da un muro di testo.
Verificato ripristinandolo, non guardandolo girare. Un dump ripristinato in un database di prova è tornato con ogni riga: clienti, documenti, voci di audit.
File piccoli
Ogni file sotto le 350 righe e suddiviso per responsabilità.
Segreti
Solo in .env, con il template dimostrato completo confrontandolo con l'oggetto delle impostazioni.
Log
Un log a rotazione verificato alla ricerca di chiavi, token e hash trapelati — non ne conteneva nessuno.
Traduzioni
Due lingue tenute esattamente allo stesso numero di chiavi da un controllo che fallisce se divergono.
Nominati di proposito — ognuno è una decisione, non una svista.
Non un middleware da ricordare, ma una funzione che ogni rotta chiama, con ventuno test il cui unico compito è dimostrare che i dati di un'altra azienda sono irraggiungibili.
Ogni stringa passa da un livello di traduzione, l'italiano usa il tu ovunque perché è così che si parla a questi clienti, e l'interfaccia è stata pensata perché anche una sezione vuota spieghi cosa ci comparirà.
Un agente AI può riportare i progressi e registrare ore sul progetto di un cliente tramite un token che non può leggere un documento, raggiungere un altro progetto o toccare l'API admin, e ogni sua azione viene registrata con l'autore marcato come token.
Il backup è stato ripristinato. Il ripiego sul retrieval è stato forzato. Il job dei promemoria è stato eseguito due volte per dimostrare che non invia due volte. All'assistente è stato chiesto di inventare un prezzo e ha rifiutato. Ognuna di queste è un controllo nel repository, non una frase in un documento.
Un portale, una dashboard o un'applicazione completa — progettati perché i tuoi clienti sappiano sempre a che punto siete.