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 | ApprovedReviewers:{{REVIEWERS}}Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 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:
- Real-time balance verification via AISP (Open Banking)
- Live payment initiation via PISP (Open Banking)
- BankID authentication for sensitive operations
This fundamentally limits offline capability — payment initiation cannot work offline by design.
| Feature | Offline Support | Priority | Notes |
|---|---|---|---|
| Send money (remittance) | Not supported | — | Requires live PISP + BankID. Cannot be queued. |
| QR payment | Not supported | — | Requires live PISP + real-time merchant verification |
| View balance | Not supported | — | AISP reads live balance from bank — no cached |
| View transaction history | Last 50 |
||
| Partial (Phase 2) | P2 | ||
| Not |
— | ||
| | | |
OfflinePhase 1 (current) offline minimum viable experience:
TODO:WhenDefinecompletelywhatoffline, theuserappshouldshowssee/doawhen"Ingencompletelynettverkstilkobling" 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,transactionorhistoryread-only(lastmode.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)
| Data | Storage | Notes |
|---|---|---|
| Auth token | AsyncStorage (in-memory during session) |
Module-level let token in lib/api.js |
| User preferences | None — fetched from server on each launch | |
| Transaction history | None — fetched on each page load | |
| Recipients | None — 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-sqliteexpo-sqlite)SQLite) |— Realmlightweight, |Expo-managed, TinyBase}}no native module ejection required.
Rationale:
SQLite
TODO:providesExplainstructuredwhy this DB was chosen (querycapability,capability for transaction filtering and recipient lookup. WatermelonDB adds syncsupport,complexityperformance,notbundleneededsize).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
| |||
| |||
| |||
|
Library: {{expo-file-system | react-native-fs}}
2.3 Secure Storage
| Data | Storage | Reason |
|---|---|---|
| Auth token | (current) → expo-secure- (Phase 2) |
|
| Refresh token | (Phase 2) |
Encrypted |
(Phase 2) |
Never in plain storage | |
AsyncStorage |
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: refresh (no push, no bidirectional sync){{BidirectionalPull-only deltacache sync}}
| Property | Value |
|---|---|
| Protocol | REST |
|
|
| Pull endpoint | GET /, GET /v1/recipients, GET /v1/notifications |
| Sync |
foreground, |
3.2 Conflict Resolution
Not
| | |
Conflict detection: Compare updated_at + server-assigned version counter.sync.
Manual conflict flow (when required):
Server returns409 Conflictwith both versionsApp stores both versions in local DBUser presented with diff UI to choose versionResolved version pushed back to server
3.3 Sync Frequency & Triggers
| Trigger | Action | Conditions |
|---|---|---|
| App foreground (from background) | Pull sync for all cached entities | Network | available,
| > 5 min since last sync | |
available |
||
| Network restored (from offline) | Full data refresh | Re-connect detected |
3.4 Partial Sync / Delta Sync
Client storeslast_sync_cursorper entity type (epoch milliseconds)Pull requests includesincecursor — server returns only changed recordsDeleted records: server maintains soft-delete withdeleted_atfor 30 daysClient applies deletions, then removes soft-deleted records from local DB
4. Sync Queue Management
QueueNo storage:sync queue for payments. SQLiteDrop explicitly sync_queuetabledoes (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:
On network restore: drain queue in FIFO orderBatch uptries to50payitemswhileperoffline:push requestOnShow error:retry"Duwithtrengerexponential backoff (1s, 2s, 4s, 8s, 16s)AftermaxRetries: 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: (Phase 2) or {{@react-native-community/netinfo}}netinfoexpo-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:
| State | UI Response |
|---|---|
| Offline | Banner: " |
| Offline (with cache) | Banner: "Viser lagret data" (Phase 2 only) |
| Reconnected | Banner: " |
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
| Test Type | Scope | Tool |
|---|---|---|
| Unit | ||
| Jest | ||
| Integration | Jest + |
|
| Manual | Airplane mode → attempt payment → verify error message | iOS/Android Simulator |
| Manual | Airplane mode → view cached history → verify banner (Phase 2) | iOS/Android Simulator |
| E2E | Full offline → reconnect flow (Phase 2) | |
|
E2E offlineManual test scenario:scenarios:
Open app online — verify data loads- Enable airplane mode on device
PerformAttemptcreate/update/deletetoactionssend money → verify error message appears (not crash)VerifyAttemptoptimisticQRUIpaymentupdates→ verify error message appearsVerifyNavigateactionstoqueuedtransaction history → verify error or empty state (inspectPhaseDB)1)- Disable airplane mode
Verify→syncverifyqueueappdrainsVerify serverrefreshes datamatches local stateautomatically
8. Storage Limits & Data Eviction Policy
Phase 1: No local storage to manage.
Phase 2 targets:
| Storage Type | Eviction Strategy | ||
|---|---|---|---|
| SQLite |
5 MB (50 |
||
| Auth token (AsyncStorage) | On |
||
| Total app storage |
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 |
|---|---|---|
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | John (AI Director) | 2026-02-23 | |
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |