Vai al contenuto

Laravel multi-tenant: cosa scrivere nella specifica prima del codice

Un mese a scrivere la specifica prima di una riga di Laravel

In un’applicazione Laravel multi-tenant, la prima domanda è cosa deve succedere quando il contesto del cliente manca, cambia o arriva in ritardo. Se questa risposta resta implicita, un coding agent può produrre codice ordinato che separa male i dati.

In Romiltec lavoro su prodotti con esigenze diverse, dalla gestione di redazioni a un SaaS verticale come RomilCar. La parola “multi-tenant” copre problemi concreti: un account deve vedere solo i propri articoli, un job deve operare per il cliente corretto, una cache non deve restituire la risposta di un altro account. Qui propongo una struttura di specifica per affrontarli prima del refactor.

Tre problemi da tenere separati

L’isolamento stabilisce quali dati appartengono a un tenant. L’autorizzazione stabilisce cosa può fare una persona su quei dati. La consistenza stabilisce quando una lettura vede una scrittura. Sono tre responsabilità diverse: mettere un filtro tenant_id in una query affronta soltanto una parte della prima.

I global scope di Eloquent aggiungono vincoli alle query dei modelli. Possono essere rimossi esplicitamente e non si applicano automaticamente a ogni query SQL costruita fuori da quei modelli. Per questo la specifica deve elencare i percorsi che leggono dati: HTTP, comandi, code, esportazioni e operazioni amministrative.

La tabella che scriverei prima di aprire il branch

Vincolo Comportamento atteso Prova utile
Contesto assente L’operazione sui dati del tenant si interrompe Una richiesta senza tenant non restituisce un elenco globale
Accesso incrociato Un identificativo valido di B non dà accesso all’utente di A Leggere e aggiornare il record di B attraverso gli endpoint di A
Job asincrono Il contesto viene ricostruito e poi ripulito Due job consecutivi, per tenant diversi, sullo stesso worker
Cache Le chiavi distinguono cliente e risorsa Due tenant chiedono lo stesso identificativo locale
Operazione privilegiata L’uscita dal perimetro è esplicita e autorizzata Il percorso amministrativo lascia una traccia verificabile

Questa tabella non descrive una classe. Descrive ciò che il sistema deve continuare a garantire, anche se cambio un middleware, il motore delle code o l’agente che mi aiuta a implementare la modifica.

Il caso che rende utile la specifica: HTTP e queue

Consideriamo un esempio: un utente del tenant A importa un catalogo. La richiesta registra l’importazione e accoda un job. Il worker, però, è un processo persistente che può aver appena lavorato per B.

Il job deve portare un identificativo del tenant, verificarne l’esistenza e ricostruire il contesto prima di accedere ai dati. A fine esecuzione il contesto va ripulito anche se c’è un’eccezione. Non basta che il middleware HTTP sia corretto: quel middleware non viene eseguito nel worker.

Se il job dipende da dati appena scritti dentro una transazione, va chiarito anche quando può partire. Laravel documenta il dispatch dopo il commit: serve a evitare che il worker cerchi dati non ancora confermati. La gestione del tenant resta comunque una responsabilità dell’applicazione.

Una replica di lettura aggiunge un’altra decisione

Se il database ha repliche asincrone, una lettura immediatamente successiva a una scrittura potrebbe arrivare prima della replica. Questo problema non si risolve rendendo più sofisticato il global scope.

L’opzione sticky di Laravel consente di usare la connessione di scrittura per letture successive nello stesso ciclo di richiesta dopo una scrittura. Non è una garanzia generale tra richieste HTTP diverse, job e cambi di nodo.

Nella specifica indicherei quali letture devono vedere subito il dato, dove vengono instradate e cosa succede se quel percorso non è disponibile. Eviterei di promettere “consistenza immediata” senza definire il confine della promessa. Anche retry e failover devono preservare l’idempotenza: ripetere una richiesta non deve duplicare un’importazione o un addebito.

Come chiedere all’agente di mettere alla prova il progetto

Prima dell’implementazione gli assegnerei una revisione circoscritta: trovare i percorsi che aggirano il contesto, proporre un caso negativo per ogni vincolo e indicare quali ipotesi richiedono una verifica nel codice esistente. Una domanda utile è: “Come potrebbe passare questo test mentre i dati di due clienti si mescolano?”.

Le risposte del modello sono ipotesi da esaminare. Il controllo più importante resta confrontarle con il sistema reale: configurazione delle connessioni, ciclo di vita dei worker, chiavi di cache, policy di autorizzazione. Una specifica molto lunga ma incompatibile con questi dettagli produce soltanto errori più documentati.

Quando il documento è abbastanza preciso

Per me è pronto quando un collega può ricavare un risultato atteso senza chiedere cosa intendessi. Per esempio: “Dato un utente di A e un articolo di B, l’endpoint di aggiornamento rifiuta l’operazione e il contenuto di B rimane invariato”. Il codice e il test possono cambiare; quel comportamento deve restare.

Al documento aggiungerei il piano di rilascio: perimetro iniziale, segnali da osservare e modo di tornare alla versione precedente senza perdere scritture. È il collegamento con la responsabilità del CTO nella revisione del codice generato: la specifica orienta il lavoro, la review e le prove stabiliscono se il risultato la rispetta.