Architettura
MVVM lato client, Firestore come single source of truth, Cloud Functions per la logica server-side.
Livelli
L’app è divisa in tre livelli con responsabilità nette:
- Client iOS (SwiftUI, MVVM) — presentazione e input utente. Scrive su Firestore, non parla mai direttamente con Gemini, Algolia o servizi di notifica.
- Firestore — store unico dello stato applicativo. Ogni pezzo di UI è un listener su una query: niente polling, niente cache manuale da invalidare.
- Cloud Functions Gen 2 — tutto ciò che richiede segreti, trust o orchestrazione: moderazione AI, notifiche push, scoring della classifica, generazione prompt, sync con Algolia.
Questa separazione permette di cambiare UI o aggiungere piattaforme (es. versione web) senza toccare le regole di dominio, che vivono nelle functions.
MVVM pratico
MVVM = Model, View, ViewModel. È il pattern idiomatico per SwiftUI: la View osserva lo stato del ViewModel, il ViewModel parla con il Model e nessuno dei tre conosce gli altri due in modo bidirezionale.
- View — SwiftUI declarative, nessun accesso diretto a Firestore. Si limitano a leggere
@Publishedproperty dal ViewModel e a chiamarne i metodi. - ViewModel —
ObservableObjectche possiede i listener Firestore, mantiene lo stato tipato e lo espone via@Published. Ogni feed (globale, following, profilo, bozze, privati, liked) ha la propria paginazione con cursore. - Model —
struct Codablecon due inizializzatori: uno daDocumentSnapshotdi Firestore, uno da JSON Algolia. Lo stesso tipo attraversa entrambe le fonti.
Le scritture lato client sono limitate a operazioni “di intenzione” (pubblica, metti like, segui). Tutto ciò che deriva da uno stato (punteggi, feed followers, indicizzazione ricerca) viene propagato dalle functions.
Modello dati
Le collezioni principali sono:
users/{uid}— profilo, flag premium, following list, token FCM per device.poems/{poemId}— poesie pubblicate (dopo moderazione).pendingPoems/{poemId}— staging pre-moderazione; i client scrivono qui, non direttamente inpoems.poems/{poemId}/comments/{commentId}— sotto-collezione commenti.dailyPrompts/{YYYY-MM-DD}— prompt giornalieri storici;dailyPrompts/currentè il puntatore letto dalla UI.leaderboards/{YYYY-MM}/entries/{uid}— punteggi mensili per autore, separati per chiave mese.reports/{reportId}— segnalazioni utente, consumate dai moderatori.
Firestore rules chiudono le scritture su tutto ciò che è derivato (poems, leaderboards, dailyPrompts) — solo il service account delle functions può modificarlo.
Realtime senza polling
Ogni ViewModel apre un addSnapshotListener sulla query che gli serve e lo tiene vivo finché la view è in scena. Quando una function scrive (es. approva una poesia, aggiorna un punteggio), la UI si aggiorna automaticamente. Il risultato pratico: nessun “pull to refresh” obbligato, nessuna race tra client che credono di essere in stato diverso.
I listener vengono chiusi esplicitamente in deinit o quando la schermata viene dismiss — un listener orfano è un costo di lettura che paga per sempre.
Ricerca
Algolia è l’indice secondario. syncAlgoliaUserRecord e le function correlate mantengono in sync utenti e poesie; il client interroga Algolia solo per la ricerca full-text e ricade su Firestore per i dettagli.
Ambienti
Produzione e staging sono due progetti Firebase distinti, identici nel codice. Il client usa uno schema Xcode dedicato con Bundle ID separato, così le due build coesistono sullo stesso device senza conflitti di notifiche o deep link.