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 @Published property dal ViewModel e a chiamarne i metodi.
  • ViewModelObservableObject che 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.
  • Modelstruct Codable con due inizializzatori: uno da DocumentSnapshot di 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 in poems.
  • 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.