FolioFOLIOdocs
AppApp
BackendBackend
Backend/State Management
05 / 07

State Management

Keeping things in sync

ZustandOffline FirstSync

Overview

All client side state lives in Zustand stores persisted to AsyncStorage. The biggest store is usePortfolioStore which holds all portfolios, assets, and the currently selected portfolio. Every mutation (add, edit, delete) happens optimistically: the UI updates immediately, and the change gets queued for server sync. The SyncService orchestrates everything: it queues operations, respects foreign key dependencies (portfolios sync before assets, assets before transactions), retries failures, and pulls delta changes from the server. A NetworkMonitor triggers queue processing whenever the device comes back online.

How it works

1

When you add an asset, for example, the store generates a client_id, updates the local state immediately (the UI reflects the change right away), persists to AsyncStorage, and enqueues a sync operation. If you are online, it processes immediately. If offline, it waits.

2

The SyncQueue is a persistent queue in AsyncStorage that survives app restarts. Each operation has a table, type (insert/update/delete), data payload, retry count, and a priority based on the table hierarchy: portfolios (1), assets (2), transactions and valuations (3), alerts (4). This ordering ensures parent records exist on the server before child records reference them.

3

Queue processing respects dependencies. If a portfolio insert fails, all asset inserts for that portfolio get skipped (not retried, just skipped) until the parent succeeds. This prevents foreign key violations. Operations retry up to 5 times. If a retry fails because of a FK violation, it does not burn a retry count because the real issue is the parent, not the child.

4

Inserts use upsert with onConflict: 'user_id,client_id' for idempotency. Updates include the current version number for optimistic locking. Deletes are soft deletes that set deleted_at.

5

Delta sync pulls changes from the server since the last successful sync timestamp stored in sync_metadata. The response gets merged into the local stores. Merge functions handle each table: mergePortfolios, mergeAssets, mergeTransactions, and so on. Server wins on conflicts (last write wins).

6

The NetworkMonitor wraps React Native's NetInfo and exposes a subscribe method. The SyncService subscribes on init. When connectivity changes from offline to online, it triggers a full sync: push pending operations, then pull latest changes.

Key decisions

Zustand over Redux or Context

Two things made this decision easy. First, we needed store access outside React: the SyncService, widget data builder, and background tasks all read store state via usePortfolioStore.getState(). Context only works inside the React tree. Second, the API surface is tiny. A Zustand store is just a function that returns an object. No action types, no reducers, no dispatch. The code is dramatically simpler.

Optimistic updates with a persistent queue

The UI never waits for the server. Every action takes effect immediately in the local store. If the server rejects it later (like an alert limit exceeded), the store rolls back. This makes the app feel fast even on slow connections. The persistent queue means nothing gets lost if you kill the app or lose connectivity mid operation.

Last write wins for conflict resolution

We considered more sophisticated conflict resolution (CRDTs, operational transforms) but last write wins covers our use case well. Folio is a single user app. The only real conflict scenario is the same user on two devices editing the same record. In that case, the most recent edit winning is the expected behavior. The version number on portfolios and assets catches accidental overwrites.

Table priority ordering for sync

Portfolios must exist on the server before assets can reference them. Assets must exist before transactions. The queue assigns priorities (1 for portfolios, 2 for assets, 3 for transactions) and processes in order. If we just processed in FIFO order, we would get constant foreign key violations whenever someone creates a portfolio and adds assets in the same offline session.

PortfolioAlertsTransactionsMarketAssistantSyncZustandstoresSync QueueSupabase

Quick Reference

Filesstores/*, services/sync/SyncService.ts, services/sync/SyncQueue.ts, services/sync/NetworkMonitor.ts
Tablessync_metadata
Edge FunctionsN/A (sync uses direct Supabase client)
Components
usePortfolioStoreusePortfolioCalculationsuseTransactionStoreuseAlertsStoreuseAssistantStoreuseMarketStoreuseSubscriptionStoreuseNotificationStoreuseSyncStoreSyncServiceSyncQueueNetworkMonitor
PreviousSubscriptionsNextiOS Widgets