Vai al contenuto

Perché ho scelto Laravel su Node per AI Multisite (e perché lo rifarei)

Perché ho scelto Laravel su Node per AI Multisite (e perché lo rifarei)

Perché ho scelto Laravel su Node per <a href=AI Multisite (e perché lo rifarei)” title=”Perché ho scelto Laravel su Node per AI Multisite (e perché lo rifarei)” loading=”eager” decoding=”async” />

Settembre 2024, davanti a due cartelle parallele sul mio MacBook: /dev/php/backend-laravel/ e /dev/js/backend-node-prototype/. La prima è il monolite Laravel 12 di AI Multisite in produzione, già con una decina di tenant attivi. La seconda è un prototipo equivalente in Node.js (Fastify + Prisma) che avevo scritto a inizio 2024 per validare un dubbio: stavo costruendo il prodotto sul framework giusto? Laravel su Node per AI Multisite è una scelta che ho preso e ripreso più volte negli ultimi dodici mesi, ed è una scelta che oggi rifarei. Non perché Node sia sbagliato, ma perché su questo prodotto, con questo team, in questo mercato, Laravel vince su quattro tradeoff concreti. Negli altri post di Production racconto le scelte di stack di AI Multisite.

Laravel su Node: il contesto del backend di AI Multisite

AI Multisite è un hub editoriale multi-tenant che orchestra contenuti, autori e pipeline AI per 40+ testate. Il backend principale ha tre responsabilità tecniche:

  1. CRUD multi-tenant pesante, con sync bidirezionale verso N installazioni WordPress via REST. Per ogni tenant: articoli, autori, redazioni, permessi, configurazioni provider AI. Tante tabelle, relazioni medio-articolate, query con scoping per tenant_id su quasi tutto.
  2. Orchestrazione di job asincroni: chiamate ai provider LLM (OpenAI, Anthropic, xAI, Google, DeepSeek, Mistral, Ollama, DeepL), upload di immagini verso WordPress, sync incrementali, esportazioni, ri-indicizzazioni.
  3. API verso il frontend Vue 3 che gira sulla dashboard utente, con autenticazione OAuth2 e RBAC granulare.

A questo si aggiunge un microservizio FastAPI separato per il workload analytics (Matomo Microservice, ClickHouse, query parallele, materialized views), scollegato dal monolite e scritto in Python proprio perché lì Python è oggettivamente più adatto. Il dibattito Laravel vs Node si gioca sul monolite editoriale, non sui servizi specialistici.

Laravel su Node: come ho impostato la scelta con quattro tradeoff

A gennaio 2024, nei mesi dell’estrazione del prodotto, mi sono posto la domanda nel modo più disonesto possibile: Laravel mi piace e lo so, ma se cambiassi mestiere alla codebase potrei guadagnarci?. Non volevo scegliere per affinità di linguaggio. Ho preso quattro tradeoff e li ho misurati su due settimane di prototipo Node parallelo.

Tradeoff 1: velocity del team

Romiltec è 5 persone. Quattro su cinque hanno PHP/Laravel come framework primario. Un dev part-time è full-stack JS, comodo con Node ma non con la complessità di un’orchestrazione async multi-provider in produzione.

Misura concreta: scrivere lo stesso modulo (sync bidirezionale articoli da WP, con queue, retry, dead-letter) ha richiesto 2 giorni in Laravel, 5 giorni in Node con Fastify più la curva di apprendimento collettivo necessaria perché il senior dev avrebbe dovuto poi mantenerlo. Su un team di cinque persone, il delta velocity è 2,5 volte. Su un team di venti, scompare. Il team di partenza è il primo vincolo del framework, non il framework il primo vincolo del team.

Tradeoff 2: ecosistema dei pattern critici

Tre cose su AI Multisite girano grazie a librerie Laravel mature, non grazie a codice mio:

  • Queue con Horizon: dashboard nativa, retry exponential backoff, dead-letter, supervisor configurabile per coda. Su Node esiste BullMQ ed è ottimo, ma la richiesta di tooling collaterale (pannello, monitoring, scaling dei worker) è più alta. Horizon è production-ready out of the box.
  • OAuth2 con Passport: il flusso completo, scope, personal access token, refresh token, è una dipendenza, non un progetto. Passport JS richiede assemblaggio.
  • Permission management con spatie/laravel-permission: ruoli, permessi, multi-guard, compatibilità con il modello User di base. Su Node si replica con CASL backend o con codice custom, ma di nuovo la superficie di test e maintenance cresce.

Misura concreta: il numero di righe scritte da me per le tre cose sopra in Laravel è ~400. Il prototipo Node equivalente, per arrivare alla stessa qualità di production-readiness, ne richiedeva ~1.800. Su un team di cinque persone bootstrap, ogni riga di codice in più è una riga in più da testare, manutenere, debuggare quando va in oncall.

Tradeoff 3: integrazione con il pattern multi-LLM

Il workload più tecnicamente delicato di AI Multisite è l’orchestrazione delle chiamate ai provider LLM. Otto provider supportati, configurazione per-tenant per-modello, retry logic specifica per tipo di errore (rate limit di OpenAI è diverso da overflow di context di Anthropic), guardrail anti-prompt-injection, logging fine per tenant del costo in token e in euro.

Su PHP esiste Prism PHP, libreria che astrae i provider LLM dietro un’interfaccia uniforme con switch dichiarativo. Ce l’avevamo già integrata e l’avevamo personalizzata per i nostri casi (temperature detection per modelli o-series, reasoning disable per modelli reasoning, output JSON strutturato).

Su Node, ai tempi del prototipo, non esisteva un equivalente con la stessa maturità per tutti gli otto provider. Le opzioni erano: scrivere il proprio adapter set (3-4 settimane di lavoro più test), oppure adottare un SDK come la libreria di Vercel AI SDK che però copriva un sottoinsieme dei nostri provider e non aveva i nostri pattern interni. Il delta lavoro era netto a favore di PHP.

Tradeoff 4: deploy e operations

Romiltec auto-ospita su VM Debian su Proxmox. Niente Vercel, niente AWS Lambda, niente managed Node hosting. Il deploy è classico: PHP-FPM più Nginx, Redis dedicato, MariaDB su cluster separato. È la stessa infrastruttura che gestisco da quindici anni in produzione, conosco i tuning, i log, i pattern di failure, gli alert.

Sostituire PHP-FPM con un cluster Node dietro Nginx è fattibile, ma porta una serie di cose nuove a presidio: gestione del cluster di processi (PM2 o systemd con worker), garbage collection del V8 da osservare in produzione, deploy con npm ci e pnpm install su VM (o build container) anziché rsync di codice PHP. Niente di insormontabile. Ma significativamente più complesso del workflow LAMP che avevo già rodato.

Misura concreta: il deploy del backend Laravel di AI Multisite, dal merge su main al go-live sul cluster, dura ~110 secondi (composer install più build assets più migrate). Il prototipo Node equivalente, con la pipeline più ricca che richiedeva, ne durava ~280. Su deploy multipli al giorno, il delta è significativo.

Laravel su Node: i punti a favore di Node che non ho ignorato

Su due cose Node è oggettivamente meglio, e l’ho riconosciuto subito.

Concorrenza I/O bound nativa. Node ha event loop async by design. Per workload con tantissime chiamate parallele a servizi esterni (50+ HTTP request concorrenti su un singolo evento), Node esce avanti. Su AI Multisite, però, la concorrenza I/O non vive nel monolite: vive nei worker di queue. E un worker Laravel più Horizon, con Octane attivo, gestisce le mie 50 chiamate concorrenti senza che il monolite ne risenta, perché il monolite serve l’API utente, non orchestra direttamente le chiamate LLM.

Tipi statici con TypeScript. Su backend di una certa dimensione, TS aggiunge sicurezza che PHP, anche con PHPStan livello 5 e Larastan, non eguaglia del tutto. È un punto vero. La compensazione, su AI Multisite, sono i DTO con Spatie Data (validazione e tipizzazione strutturata), Spatie Permission per i contratti di autorizzazione, e PHPStan come gate obbligatorio. Non è la stessa cosa di TS, ma su una codebase di ~85.000 righe di backend è abbastanza.

Octane: il punto che ha chiuso il dibattito Laravel su Node

Settembre 2024, dopo nove mesi di produzione su Laravel monolite stock, abbiamo attivato Octane sul backend principale. Octane mantiene l’app Laravel bootstrappata in memoria fra request (su FrankenPHP nel nostro caso), eliminando l’overhead di startup tipico di PHP-FPM.

Misura concreta sui nostri endpoint: la latenza p50 di un endpoint CRUD tipico è scesa da ~78ms a ~21ms. La latenza p99 sotto carico è scesa da ~310ms a ~95ms. Il numero di request al secondo che il singolo nodo regge è cresciuto di un fattore 3,5.

Quei numeri, presi senza ricontrollare lo stack scelto, avrebbero potuto essere del tutto plausibili anche su Node. La differenza è che li abbiamo ottenuti senza riscrivere niente: tre righe di config, un servizio aggiuntivo nel docker-compose, e l’API è diventata significativamente più veloce. Octane su un monolite Laravel ben scritto è uno di quegli upgrade dove il rapporto valore/effort è asimmetrico al limite del razionale.

Quando rifarei la scelta Laravel su Node al contrario

Per onestà tecnica: ci sono scenari in cui rifarei la scelta in senso opposto. Tre, in ordine di probabilità.

Primo. Se il workload del monolite diventasse dominato da streaming WebSocket multi-tenant ad alta concorrenza (chat real-time fra redazioni, broadcast di eventi push) e non da CRUD più queue, Node con Reverb come fallback Laravel non sarebbe più sufficiente. Un cluster Node dedicato al canale WebSocket sarebbe la scelta.

Secondo. Se il team raddoppiasse e il nuovo terzo dei dev fosse full-stack TS senior, il bilanciamento del primo tradeoff (velocity) si sposterebbe. Ma è uno scenario a 18-24 mesi.

Terzo. Se Prism PHP smettesse di evolversi e i nuovi provider LLM emergessero solo come SDK JS-first, l’ecosistema di adapter si sposterebbe. Per ora va nella direzione opposta: Prism aggiunge provider con cadenza regolare.

Cosa mi sono portato a casa dalla scelta Laravel su Node

Tre cose, dette anche al senior dev nel post-mortem di settembre 2024.

Prima. La scelta del framework di backend si gioca sul tuo team, sulla tua infrastruttura, sul tuo costo di operazione, prima ancora che sul framework in sé. Il “miglior framework in assoluto” è una domanda mal posta. Il miglior framework per il tuo prodotto, per il tuo team, per i tuoi tre anni di operazione è la domanda giusta.

Seconda. Su workload IO bound estremi, scolpisci il microservizio. Non snaturare il monolite. Per AI Multisite, il microservizio Python di analytics è la prova: girarlo dietro la queue del monolite Laravel ci dà il meglio dei due ecosistemi senza forzare scelte di stack a monte.

Terza. Octane su Laravel ben scritto è uno di quei moltiplicatori che ti tira fuori da una conversazione di stack senza riscrivere niente. Prima di valutare un rewrite, verifica se il tuo framework attuale ha un’ottimizzazione di runtime che non hai ancora attivato. Spesso ce l’ha.

Settembre 2024, ho cancellato il prototipo Node. La cartella /dev/js/backend-node-prototype/ non esiste più. La codebase Laravel di AI Multisite continua a evolversi, oggi è il monolite più solido che abbia mai costruito, e ogni mese mi conferma che la scelta del settembre 2024 è stata quella giusta. Non per Laravel in sé. Per la combinazione di team, infrastruttura, ecosistema multi-LLM, e obiettivi di prodotto che avevamo davanti. Lo rifarei domani mattina alle 9, esattamente nello stesso modo.