Skip to main content

Offline-First Strategy

Offline-First Strategy

Project: {{PROJECT_NAME}}Drop — Fintech Payment App Version: {{VERSION}}0.1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}John (AI Director, ALAI) Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}John Initial draft — current state (no offline support) + Phase 2 requirements

1. Offline Capability Requirements

Critical context: Drop is a pass-through payment app (PSD2 PISP model). Drop never holds customer money. All payment transactions require:

  1. Real-time balance verification via AISP (Open Banking)
  2. Live payment initiation via PISP (Open Banking)
  3. BankID authentication for sensitive operations

This fundamentally limits offline capability — payment initiation cannot work offline by design.

feed
Feature Offline Support Priority Notes
Send money (remittance)Not supportedRequires live PISP + BankID. Cannot be queued.
QR paymentNot supportedRequires live PISP + real-time merchant verification
View balanceNot supportedAISP reads live balance from bank — no cached contentbalance
View transaction history RequiredPartial (Phase 2) P1P2 Last 50 itemstransactions cached locally
CreateView draftrecipient (saved locally)list RequiredPartial (Phase 2) P1P2 SyncCached whenrecipients onlinefor reference
SearchNotification (local cache only)history Partial (Phase 2) P2 DegradedCached notification no remote resultslist
User authenticationLogin Not requiredsupported LoginBankID requires network
PushExchange notificationsrates N/APartial (Phase 2) P3 RequiresLast networkknown byrate natureshown as estimate
FileApp uploadsnavigation QueueSupported P2P1 UploadApp whenshell loads without network returns
{{FEATURE}}{{Required/Partial/Not required}}{{P1-P3}}{{Notes}}

OfflinePhase 1 (current) offline minimum viable experience:

TODO:When Definecompletely whatoffline, the userapp shouldshows see/doa when"Ingen completelynettverkstilkobling" error banner and prevents all payment operations. Previously loaded screens remain visible but all data shows as stale. No payment data is cached.

Phase 2 offline minimum blankviable screen,experience:

User can view cached data,transaction orhistory read-only(last mode.50), recipient list, and notification history without network. A banner indicates "Viser lagret data". All payment and balance operations require network.


2. Local Storage Architecture

2.1 DatabaseCurrent State (StructuredPhase Data)1)

DataStorageNotes
Auth tokenAsyncStorage (in-memory during session)Module-level let token in lib/api.js
User preferencesNone — fetched from server on each launch
Transaction historyNone — fetched on each page load
RecipientsNone — fetched on send page load

Phase 1 conclusion: Drop has no offline storage beyond the auth token. All data is fetched fresh on each mount.

2.2 Phase 2 — Planned Local Storage

Selected database: {{WatermelonDB | SQLiteexpo-sqlite (expo-sqlite)SQLite) | Realmlightweight, |Expo-managed, TinyBase}}no native module ejection required.

Rationale:

SQLite

TODO:provides Explainstructured why this DB was chosen (query capability,capability for transaction filtering and recipient lookup. WatermelonDB adds sync support,complexity performance,not bundleneeded size).for Drop's read-only cache use case.

Schema overview:overview (Phase 2):

CREATE TABLE transactions (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,               -- Example'remittance' schema| 'qr_payment'
  expandstatus perTEXT domainNOT NULL,
  amount REAL NOT NULL,
  currency TEXT NOT NULL DEFAULT 'NOK',
  recipient_name TEXT,
  created_at INTEGER NOT NULL,      -- Unix timestamp
  synced_at INTEGER NOT NULL        -- When cached
);

CREATE TABLE usersrecipients (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  emailcountry TEXT UNIQUE NOT NULL,
  avatar_urlaccount_number TEXT,TEXT NOT NULL,
  synced_at INTEGER,
  updated_at INTEGER NOT NULL
);

CREATE TABLE postsnotifications (
  id TEXT PRIMARY KEY,
  user_idtype TEXT REFERENCESNOT users(id),NULL,
  title TEXT NOT NULL,
  body TEXT,
  status TEXT DEFAULT 'published',
  is_local_draft INTEGER DEFAULT 0,
  synced_at INTEGER,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

CREATE TABLE sync_queue (
  id TEXT PRIMARY KEY,
  entity_type TEXT NOT NULL,
  entity_id TEXT NOT NULL,
  operation TEXT NOT NULL, -- 'create' | 'update' | 'delete'
  payload TEXT NOT NULL,   -- JSON
  retry_countis_read INTEGER DEFAULT 0,
  created_at INTEGER NOT NULL,
  synced_at INTEGER NOT NULL
);

-- No sync_queue table — Drop does NOT queue payment operations offline
-- Payments require real-time network by design

TODO: Define full schema for all entities.


2.2 File Storage

TypeLocationMax SizeEviction
Downloaded images{{FileSystem.cacheDirectory}}/images/200 MBLRU on cache full
Downloaded documents{{FileSystem.documentDirectory}}/docs/500 MBManual user delete
Queued upload files{{FileSystem.documentDirectory}}/uploads/1 GBOn successful upload
Temporary files{{FileSystem.cacheDirectory}}/tmp/50 MBOn app start

Library: {{expo-file-system | react-native-fs}}


2.3 Secure Storage

Data Storage Reason
Auth token {{AsyncStorage (current) → expo-secure-store}}store (Phase 2) Encrypted,Phase 2: encrypted, Keychain/Keystore
Refresh token {{expo-secure-store}}store (Phase 2) Encrypted
EncryptionBiometric key (for local DB)keys {{expo-secure-store}}store (Phase 2) Never in plain storage
UserNon-sensitive preferencesprefs AsyncStorage Non-sensitiveLanguage setting, theme preference

Rule: AnythingPayment accessed without a password must NOT be in secure storagecredentials (breaksBankID biometrictokens) authare flow)never stored locally — they are transient session tokens managed by expo-web-browser.


3. Sync Protocol Design

Critical constraint: Drop does NOT have a sync queue for payment operations. Payments cannot be created offline and synced later — this would create unacceptable financial risk and violate PSD2 requirements.

Sync scope: Read-only data (transactions, recipients, notifications) — Phase 2 only.

sequenceDiagram
    participant App
    participant LocalDB
    participant SyncQueue
    participant API

    Note over App,API: Online — foreground sync cycle(Phase App->>LocalDB: Read local data (immediate)
    App->>SyncQueue: Queue local changes2)

    App->>API: Push: POSTGET /sync/push {changes}v1/transactions?limit=50
    API-->>App: Server-applied{ changestransactions: +[...] conflicts}
    App->>LocalDB: ApplyUpsert servertransactions changes(replace all)

    App->>API: Pull: GET /sync/pull?since={timestamp}v1/recipients
    API-->>App: Remote{ changesrecipients: since[...] last pull}
    App->>LocalDB: MergeUpsert remote changesrecipients

    Note over App,API: Offline scenario

    App->>LocalDB: Read cached transactions
    LocalDB-->>App: Cached data (may be stale)
    App->>SyncQueue:App: QueueShow changes"Viser (persisted)lagret Notedata" over SyncQueue: Waits for connectivitybanner

    Note over App,API: Reconnect

    SyncQueue-App->>API: DrainGET queue/v1/transactions — push all pending(fresh)
    API-->>App: ConflictLatest resolutiondata
    App->>LocalDB: MergeOverwrite resolved statecache

3.1 Sync Strategy

Approach: {{BidirectionalPull-only deltacache sync}}refresh (no push, no bidirectional sync)

Property Value
Protocol REST + {{GraphQL subscriptions / WebSocket for live}}
Push endpointPOST /sync/push
Pull endpoint GET /sync/pull?since={unix_ms}&entities={list}v1/transactions?limit=50, GET /v1/recipients, GET /v1/notifications
Sync identifiertrigger Per-entityApp updated_atforeground, timestampafter (serversuccessful clock)payment, pull-to-refresh
PullCache deltaexpiry Only5 recordsminutes changed(data sinceolder lastthan sync5 cursormin triggers background refresh)
BatchConflict sizeresolution MaxServer 100always recordswins per push,local 200cache peris pullread-only, overwritten on sync

3.2 Conflict Resolution

Not

applicable.Drop'slocalcacheisread-only.Userscannotmodifycachedoffline.
Entity Strategy Rationale
Userdata profile LastServer Writedata Winsalways (serveroverwrites wins)Single-user edit
Post draftsLast Write Wins (client winscache on localnext draft)User owns draft
SettingsMerge (union)Non-conflicting fields
Counters (likes, views)Server-side CRDTConcurrent increments
{{Entity}}{{LWW / CRDT / Manual / Server wins}}{{Reason}}

Conflict detection: Compare updated_at + server-assigned version counter.sync.

Manual conflict flow (when required):

  1. Server returns 409 Conflict with both versions
  2. App stores both versions in local DB
  3. User presented with diff UI to choose version
  4. Resolved version pushed back to server

3.3 Sync Frequency & Triggers

available, ='data_update'
Trigger Action Conditions
App foreground (from background) Pull sync for all cached entities Network available
Mutation (create/update/delete)Immediate pushNetwork available; else queue
AppState change: background → foregroundFull sync> 5 min since last sync
Network restoredPull-to-refresh DrainFull syncdata queuerefresh AnyNetwork queued changesavailable
TimerAfter (backgroundsuccessful fetch)transaction PullRefresh synctransactions list {{EveryNetwork 15 min}}available
PushApp notificationlaunch received(authenticated) PullFull syncdata for affected entityrefresh NotificationNetwork typeavailable
Network restored (from offline)Full data refreshRe-connect detected

3.4 Partial Sync / Delta Sync

  • Client stores last_sync_cursor per entity type (epoch milliseconds)
  • Pull requests include since cursor — server returns only changed records
  • Deleted records: server maintains soft-delete with deleted_at for 30 days
  • Client applies deletions, then removes soft-deleted records from local DB

4. Sync Queue Management

QueueNo storage:sync queue for payments. SQLiteDrop sync_queueexplicitly tabledoes (survivesNOT appqueue restart)payment operations.

Queue item schema:Rationale:

interface
    SyncQueueItem
  • PSD2 {PISP id:requirements: string;payment //must UUIDbe entityType:authorized string;and //initiated 'post'in |real-time 'user'with |user etc.present
  • entityId:
  • BankID string;consent operation:is 'create'single-use |and 'update'time-limited | 'delete';cannot payload:be object;stored
  • //
  • Financial Fullrisk: entityqueuing datapayments retryCount:creates number;potential maxRetries:for number;duplicate //or 5delayed createdAt:transactions
  • number;
  • Regulatory: //Finanstilsynet Unixrequires msreal-time }payment
authorization

DrainIf strategy:

user
  1. On network restore: drain queue in FIFO order
  2. Batch uptries to 50pay itemswhile peroffline: push request
  3. OnShow error: retry"Du withtrenger exponential backoff (1s, 2s, 4s, 8s, 16s)
  4. After maxRetries: move to dead letter queue, notify user

TODO: Define user notification UXnettverkstilkobling for syncå failures.sende penger eller betale med QR."


5. Network State Detection & Handling

Library: {{@react-native-community/netinfo}}netinfo (Phase 2) or expo-network (Phase 1 alternative)

// NetworkPhase 2 — network state hook
import NetInfo from '@react-native-community/netinfo';

export function useNetworkState() {
  const [isOnline, setIsOnline] = useState(true);
  const [connectionType, setConnectionType] = useState<string>('unknown');

  useEffect(() => {
    return NetInfo.addEventListener((state) => {
      setIsOnline(state.isConnected && state.isInternetReachable);
    setConnectionType(state.type);
    });
  }, []);

  return { isOnline, connectionTypeisOnline };
}

UI behavior per state:

nettverkstilkoblingforå
State UI Response
Offline Banner: "You'reIngen offlinenettverkstilkoblingshowingbetalinger cacheder ikke tilgjengelig"
Offline (with cache)Banner: "Viser lagret data" (Phase 2 only)
Reconnected Banner: "Back onlineTilkobletsyncing.oppdaterer..." (auto-dismiss 3s)
SlowPayment connectionattempted offline NoError extramodal: UI"Du (handletrenger transparently)
Syncsende in progressSubtle indicator (not blocking)penger"

Current Phase 1 behavior: API calls fail with network error → fetch() throws → .catch() shows error state. No proactive network detection.


6. Data Flow: Online vs Offline

flowchart TD
    UserAction["User Action"] --> CheckFeature{Payment or\nview feature?}

    CheckFeature -->|"Payment (Send/QR)"| CheckNetwork{Network\nAvailable?}
    CheckFeature -->|"View data"| CheckNetworkView{Network\nAvailable?}

    CheckNetwork -->|Yes|"Yes"| DirectAPI[InitiatePayment["SendInitiate topayment via API\ndirectly"]n(BankID DirectAPI+ -->|Success| UpdateLocal[PISP)"Update local DB"]
    DirectAPI -->|Error| QueueAction["Queue action\n+ optimistic update"]
    CheckNetwork -->|No|"No"| QueueActionShowError["Error: QueueActionIngen tilkobling\nBetalinger krever nettverk"]

    CheckNetworkView -->|"Yes"| UpdateLocalFetchFresh["Fetch UpdateLocalfresh data from API\nUpdate local cache (Phase 2)"]
    CheckNetworkView -->|"No"| UpdateUI[ShowCached["UpdateShow UI\cached data\n(optimistic)Phase 2) or empty state (Phase 1)"]

    InitiatePayment -->|"Success"| ShowConfirmation["Show payment confirmation"]
    InitiatePayment -->|"Failure"| ShowPaymentError["Show payment error\n(retry option)"]

    style UpdateUIShowError fill:#f8d7da
    style ShowPaymentError fill:#f8d7da
    style ShowCached fill:#fff3cd
    style ShowConfirmation fill:#d4edda
    style QueueAction fill:#fff3cd

7. Testing Strategy for Offline Scenarios

handling(network
Test Type Scope Tool
Unit SyncAPI queueerror operations Jest
UnitConflict resolution logicfailure) Jest
Integration DBCache read/writeread withwhen mock networkoffline Jest + in-memorymock DBSQLite (Phase 2)
ManualAirplane mode → attempt payment → verify error messageiOS/Android Simulator
ManualAirplane mode → view cached history → verify banner (Phase 2)iOS/Android Simulator
E2E Full offline → reconnect flow (Phase 2) Detox / Maestro
ManualNetwork conditions simulatorNetwork Link Conditioner (iOS), tc (Android emulator)

E2E offlineManual test scenario:scenarios:

  1. Open app online — verify data loads
  2. Enable airplane mode on device
  3. PerformAttempt create/update/deleteto actionssend money → verify error message appears (not crash)
  4. VerifyAttempt optimisticQR UIpayment updates→ verify error message appears
  5. VerifyNavigate actionsto queuedtransaction history → verify error or empty state (inspectPhase DB)1)
  6. Disable airplane mode
  7. Verify syncverify queueapp drains
  8. Verify serverrefreshes data matches local stateautomatically

8. Storage Limits & Data Eviction Policy

Phase 1: No local storage to manage.

Phase 2 targets:

overwrite promptuser
Storage Type Soft LimitHard Limit Eviction Strategy
SQLite DBcache (transactions) 5 MB (50 MBtransactions max) 200Always MB Evictwith recordslatest olderfrom than 30 daysserver
ImageSQLite cache (recipients) 1501 MB 300Always MBLRU evictionoverwrite
DocumentSQLite cache (notifications) 2002 MB 500Always MBoverwrite
Auth token (AsyncStorage) UserNegligible On tologout clear/ token expiry
Total app storage 500< 30 MB 1Alert GB Warnif user,device offerstorage cleanup< 200 MB

Low storage alert: When device storage < 500 MB free, reduce cache limits by 50%.

User-initiated cleanup: Settings → Storage → Clear Cache option.


9. Error Handling & User Feedback

Error User Feedback Recovery Action
SyncNetwork pushfailure failedon payment Toast:"Du "Couldn'ttrenger syncnettverkstilkobling for willå retry"sende penger eller betale med QR" Auto-retryRetry withbutton, backoffcheck connection
ConflictNetwork detectedfailure on data load Modal:Empty "Updatestate conflictwith retry please resolve"button Manual resolution flowPull-to-refresh
QueueNetwork overflowfailure (>500on items)login Warning"Sjekk bannernettverkstilkoblingen din" PartialRetry push, user notifiedlogin
LocalStale DBcache corruption(Phase 2) Alert:"Viser lagret data fra {time}"Storage error — please reinstall" OfferAuto-refresh freshon installreconnect
StorageSync limitfailure reached(Phase 2) AlertSilent with cleanupapp CTAshows cached data UserAuto-retry clearson cachenext foreground

Approval

Role Name Date Signature
Author John (AI Director) 2026-02-23
Mobile Lead
Backend Lead
Product Owner