Come ho progettato Bilancino: Architettura Full-Stack con Angular 21, Signals e Supabase AI
Deep-dive tecnico sull'architettura di Bilancino: Signals, Component Store, caching deterministico con mutex, Edge Functions con Gemini AI e strategie per azzerare i cold start.
Quando ho deciso di sviluppare Bilancino — un’applicazione web mobile-first per la gestione del bilancio familiare — il mio obiettivo primario non era solo creare uno strumento utile, ma realizzare una vetrina architetturale moderna.
In oltre 20 anni di ingegneria software ho visto cambiare e sovrapporsi decine di pattern. Con Bilancino volevo dimostrare come la combinazione di Angular 21 (Standalone + Signals), un’architettura Component-Store disaccoppiata e un backend servito da Supabase + AI Serverless possa offrire prestazioni native, azzerare il codice boilerplate e garantire una manutenzione impeccabile.
In questo articolo esploreremo le scelte tecniche più significative del progetto, accompagnate da snippet di codice reali.
1. Stato Reattivo senza Boilerplate: Signals & Scoped Component Store
Una delle prime decisioni architetturali è stata eliminare NgRx o Redux. Per un’applicazione di questa portata, uno store globale centralizzato avrebbe introdotto una complessità inutile e decine di file tra azioni, reducer ed effetti.
Ho optato invece per il pattern Component Store accoppiato ai Signals di Angular:
- Scoped Store: Ogni feature complessa (es.
SpeseStore,AnalisiStore) possiede una propria classe@Injectable()registrata esclusivamente nel provider del componente (providers: [SpeseStore]). Quando la pagina si smonta, lo store e la sua memoria vengono distrutti automaticamente. - Componenti Thin Shell: I componenti Angular (
.ts) agiscono come puri orchestratori grafici e non contengono logica di business.
Caching globale reattivo con Mutex (Lock Asincrono)
Per evitare round-trip inutili verso il database (es. ricaricare le categorie ad ogni apertura di modali o cambi di pagina), ho creato dei servizi di stato providedIn: 'root' che implementano un lock asincrono basato su Promise per prevenire fetch concorrenti:
@Injectable({ providedIn: 'root' })
export class CategorieStateService {
readonly categorie = signal<CategoriaItem[]>([]);
private loaded = false;
private loadingPromise: Promise<void> | null = null; // Mutex
async ensureLoaded(): Promise<void> {
if (this.loaded) return;
if (this.loadingPromise) return this.loadingPromise; // Se un fetch è in corso, accodati
this.loadingPromise = this.fetchAndStore();
await this.loadingPromise;
this.loadingPromise = null;
}
private async fetchAndStore(): Promise<void> {
const { data, error } = await supabase
.from('categorie')
.select('id, nome, colore, budget, tipo_movimento, icona')
.order('nome');
if (!error && data) {
this.categorie.set(data.map(row => this.normalizeCategoria(row)));
this.loaded = true;
}
}
}
2. Intercettazione Custom di Supabase senza HttpClient
Il client JavaScript ufficiale di Supabase (@supabase/supabase-js) si basa sulla funzione nativa fetch del browser anziché su HttpClient di Angular. Di conseguenza, i classici HttpInterceptor non funzionano.
Per gestire un Loading Service globale con contatore di richieste concorrenti, ho iniettato una funzione fetch customizzata direttamente nell’inizializzazione del client Supabase:
// Registro fuori dalla Dependency Injection di Angular
export const loaderRegistry = {
loadingService: null as any
};
export const supabase = createClient(supabaseUrl, supabaseKey, {
global: {
fetch: async (url, options) => {
// Attiva lo spinner globale
loaderRegistry.loadingService?.show();
try {
return await fetch(url, options);
} finally {
// Garantisce lo spegnimento anche in caso di eccezioni di rete
loaderRegistry.loadingService?.hide();
}
},
},
});
All’avvio dell’app (app.ts), colleghiamo il singleton LoadingService al registro: loaderRegistry.loadingService = inject(LoadingService);. Qualsiasi query REST, login o mutazione SQL attiva automaticamente lo spinner di caricamento in modo trasparente.
3. Natural Language Processing & AI su Edge Functions (Deno)
Una delle feature più amate dagli utenti è il Quick Add basato su Intelligenza Artificiale: l’utente può scrivere una frase del tipo “Abbonamento Spotify 10,99€ ricorrrente #svago” e l’AI estrae e pre-compila tutti i campi del form.
Strategia Anti-Cold Start (Pre-warming Proattivo)
Le Edge Function serverless di Supabase soffrono del classico problema del “cold start” quando rimangono inattive. Per garantire risposte istantanee, ho implementato un meccanismo di pre-warming silenzioso (fire-and-forget) che invia un ping di riscaldamento quando l’utente fa il login o apre un dialog:
// In AiService (Angular)
prewarm(): void {
const now = Date.now();
if (now - this.lastPrewarmTime < 5 * 60 * 1000) return; // Throttling 5 minuti
this.lastPrewarmTime = now;
const urls = this.getEdgeFunctionUrl();
urls.forEach((url) => {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ping: true }), // Ping leggero
}).catch(() => {}); // Intercetta e ignora silenziosamente
});
}
Nella Edge Function in Deno, se il body contiene { ping: true }, la funzione risponde immediatamente con un 200 OK bypassando la chiamata a Gemini:
// In Supabase Edge Function (Deno)
Deno.serve(async (req) => {
const body = await req.json();
if (body.ping === true) {
return new Response(JSON.stringify({ pong: true }), {
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// ... Elaborazione prompt con Google Gemini ...
});
Failover Multi-Modello (Anti-503)
Per evitare disservizi dovuti ai limiti di rate-limiting o sovraccarichi temporanei dei server di Google, l’Edge Function implementa un failover automatico sequenziale: prova prima il modello primario (gemini-3.5-flash) e, in caso di errore, effettua una transizione istantanea sul modello di riserva (gemini-3.1-flash-lite).
4. Modalità Privacy (Incognito) Reattiva
Ispirandomi alle principali applicazioni di Home Banking, ho introdotto una Modalità Privacy attivabile dalla Topbar. Quando è attiva, tutti i dati finanziari sensibili vengono oscurati con la stringa •••• €.
La sfida tecnica è stata propagare questa trasformazione non solo nel DOM HTML, ma anche all’interno dei canvas dei grafici dinamici (ApexCharts).
Il PrivacyService espone un Signal obscureAmounts = signal<boolean>(false) persistito in localStorage. I grafici ri-generano reattivamente i loro dati e tooltip sfruttando le funzioni computed() di Angular:
readonly chartKey = computed(
() => `${this.themeService.theme()}_${this.privacy.obscureAmounts()}`
);
// Nel formatter del Tooltip ApexCharts:
formatter: (value: number) => {
if (this.privacy.obscureAmounts()) {
return '•••• €';
}
return `€ ${value.toLocaleString('it-IT')}`;
}
5. Mobile-First UX e Gestures Native
L’interfaccia di Bilancino è stata costruita con un approccio Mobile-First rigoroso. Su dispositivi mobili la navigazione si sposta su una Bottom Navigation Bar, mentre per la gestione dei movimenti ho integrato le Swipe Gestures native sfruttando il modulo @angular/cdk/drag-drop:
- Swipe a Sinistra (←): Attiva la cancellazione della riga con vibrazione tattile (
navigator.vibrate). - Swipe a Destra (→): Apre la modalità di modifica rapida.
onSwipeEnded(event: CdkDragEnd<any>, item: SpesaItem): void {
const swipeDistance = event.distance.x;
const threshold = 100; // Pixel di tolleranza
if (swipeDistance < -threshold) {
this.privacy.vibrate(25);
this.deleteClicked.emit(item);
} else if (swipeDistance > threshold) {
this.privacy.vibrate(25);
this.editClicked.emit(item);
}
event.source.reset(); // Riallinea istantaneamente la riga
}
Conclusioni & Lezioni Apprese
Sviluppare Bilancino mi ha confermato una grande verità: l’ecosistema moderno di Angular, combinato con i giusti servizi Cloud/AI, non ha nulla da invidiare ai framework apparentemente più ‘leggeri’ come React o Svelte.
Grazie ai Signals, OnPush Change Detection e all’eliminazione dei moduli monolitici, l’applicazione garantisce:
- Bundle size ridotto: caricamento lazy di modali e grafici.
- 0 ricaricamenti di pagina: aggiornamenti fluidi a 60 fps.
- Codice manutenibile: separazione netta tra UI, Store e Caching.
Puoi provare l’applicazione dal vivo su bilancino.netlify.app o dare un’occhiata agli altri progetti sul mio portfolio!