Technical documentation · v2.7.7

Architecture

FamSync is a local-first Flutter application. Each device keeps a full replica of its family's data in a local encrypted SQLite database; Cloud Firestore is used only to sync shared state across co-parents. The vault uses AES-256 on-device with biometric-derived keys. Authentication is anonymous — no email, no profile, no PII.

4
Layers
AES-256
Vault encryption
EU
Firestore region
365d
Inactive auto-delete

1. Overview

FamSync is built on a four-layer stack designed around three principles: data locality (your device is the source of truth), end-to-end encryption for sensitive items (notes, vault documents), and realtime sync only for shared state (calendar, expenses, messages, photos).

  1. Storage layer: Drift (SQLite) on device + Cloud Firestore for shared family state. EU region.
  2. Crypto layer: AES-256 for vault content, EncryptedSharedPreferences (AES-256-GCM via Keystore) for tokens, SHA-256 to derive vault keys from the family identity.
  3. Identity layer: Firebase Authentication with signInAnonymously() — zero PII, retry-with-backoff to handle offline boot.
  4. Sync layer: Riverpod providers that merge Drift local streams with Firestore snapshots; last-write-wins by server timestamp.
  5. Family units: a family can hold up to 8 roles (parent, grandparent, babysitter, lawyer…). A single role can span multiple devices; a single device can hold multiple roles.

Architecture data-flow diagram. Inside your device: Drift SQLite (FamSyncDatabase) for local storage, AES-256 vault with SHA-256 familyId key derivation, and Riverpod providers for reactive UI. Two-way sync to Firebase Authentication (anonymous sign-in with retry backoff) and Cloud Firestore (families collection, EU region). The family circle on the right shows up to 8 roles co-sharing state.

FamSync architecture data-flow On-device Drift / Vault / Riverpod feed into Firebase Authentication and Cloud Firestore which sync state across up to 8 family roles. YOUR DEVICE Flutter · local-first Drift · SQLite FamSyncDatabase events · expenses · messages · … AES-256 Vault CryptoHelper + encrypt SHA-256(familyId) → key Riverpod providers reactive UI · DI last-write-wins + ts Firebase Authentication signInAnonymously() retry 2s/4s/6s backoff zero PII · UID-only Cloud Firestore families/{familyId}/… EU region · google-cloud real-time snapshot() FCM push (notifications) Family up to 8 roles Device A Device B Device C more roles Solid blue = authoritative read/write path · Soft gray = reactive stream · Realtime family sync via Firestore snapshots

2. Local-first storage — Drift (SQLite)

Each device owns a full replica of its family's data in a local SQLite database through drift_flutter. The schema lives in lib/core/data/database.dart as FamSyncDatabase extends _$FamSyncDatabase, declared with @DriftDatabase(tables: […]).

Why local-first: the app works offline (in a tunnel, on a plane, with no signal). Reads are instant and don't compete for bandwidth. Writes are optimistic: applied to Drift first, then mirrored to Firestore in the background.

Tables (non-exhaustive)

Observers

Drift stream listeners (.watch()) propagate local writes to Riverpod providers in lib/di/repository_providers.dart. The UI subscribes to providers, not to Firestore directly.

3. AES-256 vault (on-device encryption)

Sensitive content — private notes, locked documents — is encrypted on the device with AES-256 using the encrypt Dart package. The implementation lives in lib/core/util/crypto_helper.dart.

// lib/core/util/crypto_helper.dart class CryptoHelper { static String? encrypt(String? p, String familyId) { ... } static String? decrypt(String? c, String familyId) { ... } }

Key derivation

Storage of decrypted key material: on Android, secrets (auth prefs, familyId, device role) are persisted via flutter_secure_storage's encryptedSharedPreferences: true option, which uses Android Keystore + AES-256-GCM under the hood. lib/core/data/secure_storage.dart.

4. Firebase Authentication (anonymous, zero-PII)

Identity is established via FirebaseAuth.instance.signInAnonymously() — the user never provides an email, phone number, display name, or password. The returned uid is the only persistent identifier used across devices and used by Firestore security rules.

// lib/core/auth/auth_service.dart final _auth = FirebaseAuth.instance; try { await _auth.signInAnonymously(); } catch (_) { await Future.delayed(Duration(seconds: 2)); } // up to 3 attempts: 2s, 4s, 6s backoff

Why anonymous

The single social exception

lib/core/auth/social_auth_service.dart implements Google Sign-In as an optional fallback. If a user's anonymised UID is lost (factory reset on a long-disused device), a Google-linked UID can recover membership of the family the user has joined in the last 90 days. The Google profile is never displayed in-app, never shared, and never required.

5. Cloud Firestore — shared family state

Cloud Firestore is the only networked state layer; the rest is local. Collections are partitioned per family:

families/{familyId}/events // custody calendar families/{familyId}/expenses // shared ledger families/{familyId}/messages // chat + tone families/{familyId}/photos // album pointers families/{familyId}/safeNotes // vault ciphertext families/{familyId}/members/{uid} // role + permissions + FCM push token

FCM push tokens are stored alongside each member record and rotated on every cold start (lib/core/notifications/fcm_service.dart). No separate fcm/ subcollection.

Region

All Firestore data lives in a Google Cloud region inside the European Union, configured by lib/core/data/firebase_config.dart. No data leaves the EU.

Real-time sync

Repository classes subscribe via .snapshots() and merge into Drift via familyInfoStreamProvider and similar. Conflict resolution is last-write-wins by server_timestamp() — straightforward, predictably monotonic, and visible to the user through a small "edited" badge.

Security rules

Firestore security rules (see firestore.rules) gate reads/writes to members/{uid}.role. Auxiliary roles (grandparents, babysitters, lawyer — up to 8 per family) have granular per-area permissions enforced server-side, not just client-side.

6. Offline & sync

The device is offline-tolerant by design:

  1. All UI reads are served by Drift (the local SQLite database) — never blocked by network.
  2. Writes are applied to Drift optimistically; a background worker mirrors them to Firestore and FCM.
  3. _deferredBootstrap() (in lib/main.dart) intentionally delays the first read of familyId, FirestoreConfig.configure(), and AuthService.signIn() until after the first frame. This keeps cold start under 800ms on iPhone 8.
  4. On reconnect, Drift familyInfoStreamProvider re-syncs against Firestore snapshots — the UI converges within a few hundred ms.
Rule of thumb: a user at the bottom of a tunnel should be able to open FamSync and see today's custody calendar. That's the bar.

7. Push notifications

Firebase Cloud Messaging handles push. Tokens are aligned server-side via lib/core/notifications/fcm_service.dart after onboarding, when familyId and deviceRole are known. Notification targeting (lib/core/notifications/notification_targeter.dart) distributes pushes by role and event category — only the parent's phone chimes for a calendar handoff; only the accountant-role phone chimes for a new expense.

8. Data retention and DSR

  1. An active family retains its Firestore data indefinitely while at least one device is in active use.
  2. Inactive families (no live signed-in device for 365 days) are automatically purged by a scheduled Cloud Function. Drift on those orphaned devices re-prompts the user to either re-join or create a new family.
  3. The user may at any time request a Data Subject Request (DSR) from the DSR / GDPR page: export (JSON + ZIP), correct, or fully delete. Requests are answered within 30 days.
Open-source disclosure: the source code, schema, and security rules will be published once the production audit completes. Until then, this page is the authoritative technical description.

9. Stack — packages and versions

Flutter core

Riverpod 2.6 · go_router 14.8 · Drift 2.x via drift_flutter · encrypt 5.0 · flutter_secure_storage 9.2 · Firebase Auth 5.7 · Cloud Firestore 5.6 · Firebase Cloud Messaging.

Why this stack

Drift gives reactive typed SQL with row-level Diff — ideal for live UI sync without server round-trips. The encrypt package is small, audited, and pure Dart — no platform channels to audit. Riverpod's generator-based providers give compile-time DI check. Firebase Auth anonymous gives a zero-PII account layer that's still revocable server-side.

10. Reliability tests

The integrity of the vault, the Drift/Firestore merge order, and the AES key derivation are covered by automated tests under test/:

← Torna al Privacy Hub · Subprocessors · Terms of Service