Portale Clienti • Isolamento dei Dati • AI Applicata

MC Customer
Portal

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.

Limitato all'azienda su ogni rotta Approvazioni come registrazioni Assistente che conosce il progetto Bilingue IT / EN

Ruolo

Unico architetto e sviluppatore

Prodotto, backend, frontend, AI, infrastruttura

Realizzato

Settembre 2026

Otto fasi

Stato

Completato, in produzione

Sulla mia infrastruttura · accesso clienti

Stack

FastAPI · React

PostgreSQL · pgvector · Tailwind · Hetzner

Numeri verificati

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.

283

controlli automatici, tutti superati

107

endpoint API su 131 operazioni

38

tabelle del database in 8 migrazioni

23'061

righe di codice applicativo

666

chiavi di traduzione, in ciascuna delle due lingue

96

componenti React

15

tipi di azione sotto audit

12

template email, localizzati

Il problema

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.

Il cuore ingegneristico

L'isolamento è l'architettura, non uno strato sopra

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:

01

Un cliente raggiunge la sua azienda, e solo la sua azienda

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.

02

I file non sono mai serviti dal web server

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.

03

L'automazione riceve un token che sa fare un solo lavoro

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

Vedi come cliente

È 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.

Che cosa riceve davvero il cliente

Un solo posto per il progetto, i documenti, i pagamenti e le domande — pensato per chi non è tecnico.

Un progetto che si spiega da solo

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.

Documenti che si comportano da documenti

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.

Approvazioni che sono registrazioni

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.

Un modo per rimandare il materiale

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.

Pagamenti leggibili

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.

Un assistente che conosce il progetto

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

Ammette ciò che non sa.

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.

Il problema del contesto, e cosa l'ha risolto

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.

contesto ≤ budget

Tutto entra per intero

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.

contesto > budget

Ricerca per similarità con pgvector

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.

Deployment

Un server, un dominio, niente di esposto che non debba esserlo.

customers-portal.matteocola.com

▼
nginx

TLS · HSTS · CSP · serve la SPA e gli asset statici

▼ /api/ai/
buffering off
▼ /api/
uvicorn · FastAPI

systemd, 2 worker · solo localhost

▼
▼
PostgreSQL

+ pgvector · 38 tabelle

storage/

fuori dalla web root

timer systemd
  • 02:40 backup
    pg_dump + upload, conservazione 30 giorni
  • 03:20 pulizia
    upload abbandonati, conversazioni oltre la conservazione
  • 08:30 promemoria
    un sollecito per fattura prima della scadenza, uno dopo

Perché la rotta AI è separata

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.

Un backup che è stato ripristinato

Verificato ripristinandolo, non guardandolo girare. Un dump ripristinato in un database di prova è tornato con ogni riga: clienti, documenti, voci di audit.

Stack tecnico

Backend

  • FastAPI — l'API, 107 endpoint
  • SQLAlchemy 2.0 + Alembic — modelli tipizzati, migrazioni
  • PostgreSQL 16 + pgvector — dati ed embedding in un unico database
  • argon2 · PyJWT · slowapi — hashing delle password, token, rate limiting
  • PyMuPDF + LibreOffice headless — testo, miniature, conversione Office
  • Resend · OpenAI — email e l'assistente

Frontend

  • React 18 + Vite 7 — 96 componenti
  • Tailwind 3 — stile
  • react-router 7 — routing
  • react-pdf — visualizzatore integrato con navigazione e zoom
  • react-markdown — con sanificazione
  • Caricamento differito — bundle iniziale da 118 kB gzip; i 103 kB del visualizzatore arrivano solo quando si apre un documento

Pratica ingegneristica

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.

Limiti dichiarati

Nominati di proposito — ognuno è una decisione, non una svista.

  • • Single-tenant per scelta. Uno studio, un account admin definito nell'ambiente, nessuna gerarchia organizzativa. Aggiungere un secondo sviluppatore significa aggiungere un vero sistema di ruoli.
  • • Nessuna firma elettronica qualificata. Le prese visione registrano nome, ora e IP, e l'interfaccia dice esattamente questo. Tutto ciò che ha valore legale è un PDF firmato ricaricato nel portale.
  • • La conversione dipende da LibreOffice installato. Senza, gli upload DOCX e PPTX funzionano comunque e l'originale resta scaricabile; manca solo l'anteprima, e la riga admin lo segnala.
  • • Le conversazioni con l'assistente escono dalla Svizzera. Domande e contesto del progetto vanno a OpenAI negli Stati Uniti. La pagina privacy lo dice in entrambe le lingue, e la funzione può restare completamente spenta omettendo una sola chiave.
  • • La QR-fattura non viene generata. Le fatture sono PDF che carico io e che la contengono già. Generare QR-fatture svizzere è stato escluso di proposito anziché costruito a metà.
  • • Nessuna app mobile. L'interfaccia è responsive e funziona da telefono; non è pacchettizzata come app.

Che cosa dimostra questo progetto

L'isolamento dei dati come proprietà strutturale

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.

Costruire per chi non è tecnico

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à.

Automazione al guinzaglio

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.

Verifica anziché affermazione

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.

Stai costruendo qualcosa in cui il rapporto con il cliente è il prodotto?

Un portale, una dashboard o un'applicazione completa — progettati perché i tuoi clienti sappiano sempre a che punto siete.