Offline-First Strategy
Offline-First Strategy
Project:
Drop — Fintech Payment App{{PROJECT_NAME}} Version:0.1.0{{VERSION}} Date:2026-02-23{{DATE}} Author:John (AI Director, ALAI){{AUTHOR}} Status: Draft | In Review | Approved Reviewers:Alem Bašić (CEO){{REVIEWERS}}
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | Initial draft |
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 |
|---|---|---|---|
| Required | P1 | Last 50 items | |
| Create draft ( |
Required | P1 | Sync when online |
| Search (local cache only) | Partial | P2 | Degraded — no remote results |
| User authentication | Not |
— | |
| Login | |||
{{FEATURE}} |
{{Required/Partial/Not required}} |
{{P1-P3}} |
{{Notes}} |
Phase 1 (current) offlineOffline minimum viable experience:
WhenTODO: Define what the user should see/do when completelyoffline, the app shows a "Ingen nettverkstilkobling" 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— viableblank experience:
User can viewscreen, cachedtransactiondata,historyor(lastread-only50), recipient list, and notification history without network. A banner indicates "Viser lagret data". All payment and balance operations require network.mode.
2. Local Storage Architecture
2.1 Current StateDatabase (PhaseStructured 1)
| | |
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 StorageData)
Selected database: {{WatermelonDB | SQLite (expo-sqliteSQLite)expo-sqlite) —| lightweight,Realm Expo-managed,| no native module ejection required.TinyBase}}
Rationale:
providesTODO:
structuredExplain why this DB was chosen (querycapability for transaction filtering and recipient lookup. WatermelonDB addscapability, synccomplexitysupport,notperformance,neededbundleforsize).Drop's read-only cache use case.
Schema overview (Phase 2):overview:
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'remittance'Example |schema 'qr_payment'— statusexpand TEXTper NOT 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
);domain
CREATE TABLE recipientsusers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
countryemail TEXT UNIQUE NOT NULL,
account_numberavatar_url TEXT NOT NULL,TEXT,
synced_at INTEGER,
updated_at INTEGER NOT NULL
);
CREATE TABLE notificationsposts (
id TEXT PRIMARY KEY,
typeuser_id TEXT NOTREFERENCES NULL,users(id),
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,
is_readentity_id TEXT NOT NULL,
operation TEXT NOT NULL, -- 'create' | 'update' | 'delete'
payload TEXT NOT NULL, -- JSON
retry_count 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
| Type | Location | Max Size | Eviction |
|---|---|---|---|
| Downloaded images | {{FileSystem.cacheDirectory}}/images/ |
200 MB | LRU on cache full |
| Downloaded documents | {{FileSystem.documentDirectory}}/docs/ |
500 MB | Manual user delete |
| Queued upload files | {{FileSystem.documentDirectory}}/uploads/ |
1 GB | On successful upload |
| Temporary files | {{FileSystem.cacheDirectory}}/tmp/ |
50 MB | On app start |
Library: {{expo-file-system | react-native-fs}}
2.3 Secure Storage
| Data | Storage | Reason |
|---|---|---|
| Auth token | {{expo-secure- |
|
| Refresh token | {{expo-secure- |
Encrypted |
{{expo-secure- |
Never in plain storage | |
AsyncStorage |
Rule: PaymentAnything credentialsaccessed without a password must NOT be in secure storage (BankIDbreaks tokens)biometric areauth never stored locally — they are transient session tokens managed by flow).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
App->>LocalDB: Read local data (Phaseimmediate)
2)App->>SyncQueue: Queue local changes
App->>API: GETPush: POST /v1/transactions?limit=50sync/push {changes}
API-->>App: {Server-applied transactions:changes [...]+ }conflicts
App->>LocalDB: UpsertApply transactionsserver (replace all)changes
App->>API: Pull: GET /v1/recipientssync/pull?since={timestamp}
API-->>App: {Remote recipients:changes [...]since }last pull
App->>LocalDB: UpsertMerge recipientsremote changes
Note over App,API: Offline scenario
App->>LocalDB: Read cached transactions
LocalDB-->>App: Cached data (may be stale)
App->>App:SyncQueue: ShowQueue "Viserchanges lagret(persisted)
data"Note bannerover SyncQueue: Waits for connectivity
Note over App,API: Reconnect
App-SyncQueue->>API: GETDrain /v1/transactionsqueue (fresh)— push all pending
API-->>App: LatestConflict dataresolution
App->>LocalDB: OverwriteMerge cacheresolved state
3.1 Sync Strategy
Approach: Pull-only{{Bidirectional cachedelta refresh (no push, no bidirectional sync)sync}}
| Property | Value |
|---|---|
| Protocol | REST + {{GraphQL subscriptions / WebSocket for live}} |
| Push endpoint | POST /sync/push |
| Pull endpoint | GET / |
| Sync |
updated_at |
3.2 Conflict Resolution
Not
applicable.
| Entity | Strategy | Rationale |
|---|---|---|
| User profile | Last Write Wins (server wins) | Single-user edit |
| Post drafts | Last Write Wins (client wins on local |
User |
| Settings | Merge |
Non-conflicting fields |
| Counters (likes, views) | Server-side CRDT | Concurrent increments |
{{Entity}} |
{{LWW / CRDT / Manual / Server |
{{Reason}} |
Conflict sync.detection: Compare updated_at + server-assigned version counter.
Manual conflict flow (when required):
- Server returns
409 Conflictwith both versions - App stores both versions in local DB
- User presented with diff UI to choose version
- Resolved version pushed back to server
3.3 Sync Frequency & Triggers
| Trigger | Action | Conditions |
|---|---|---|
| App foreground |
Pull sync |
Network |
| Mutation (create/update/delete) | Immediate push | Network available; else queue |
AppState change: background → foreground |
Full sync | > 5 min since last sync |
{{Every |
||
3.4 Partial Sync / Delta Sync
- Client stores
last_sync_cursorper entity type (epoch milliseconds) - Pull requests include
sincecursor — server returns only changed records - Deleted records: server maintains soft-delete with
deleted_atfor 30 days - Client applies deletions, then removes soft-deleted records from local DB
4. Sync Queue Management
NoQueue sync queue for payments.storage: DropSQLite explicitlysync_queue doestable NOT(survives queueapp payment operations.restart)
Rationale:Queue item schema:
interface SyncQueueItem {
id: string; // UUID
entityType: string; // 'post' | 'user' | etc.
entityId: string;
operation: 'create' | 'update' | 'delete';
payload: object; // Full entity data
retryCount: number;
maxRetries: number; // 5
createdAt: number; // Unix ms
}
Drain strategy:
PSD2OnPISPnetworkrequirements:restore:paymentdrainmust be authorized and initiatedqueue inreal-timeFIFO order- Batch up to 50 items per push request
- On error: retry with
userexponentialpresentbackoff (1s, 2s, 4s, 8s, 16s) BankIDAfterconsentmaxRetries:ismovesingle-usetoanddeadtime-limitedletter—queue,cannotnotifybe storeduserFinancial
TODO: queuingDefine paymentsuser createsnotification potentialUX for duplicatesync or delayed transactions
If user tries to pay while offline: Show error: "Du trenger nettverkstilkobling for å sende penger eller betale med QR."failures.
5. Network State Detection & Handling
Library: {{@react-native-community/netinfonetinfo}} (Phase 2) or expo-network (Phase 1 alternative)
// Phase 2 — networkNetwork 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 { isOnlineisOnline, connectionType };
}
UI behavior per state:
| State | UI Response |
|---|---|
| Offline | Banner: " |
| Reconnected | Banner: " |
| Sync |
Subtle indicator (not blocking) |
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| DirectAPI["Yes"|Send InitiatePayment["Initiate payment viato API\n(BankIDndirectly"]
+DirectAPI PISP)-->|Success| UpdateLocal["Update local DB"]
DirectAPI -->|Error| QueueAction["Queue action\n+ optimistic update"]
CheckNetwork -->|"No"|No| ShowError["Error:QueueAction
Ingen tilkobling\nBetalinger krever nettverk"]
CheckNetworkViewQueueAction -->|"Yes"| FetchFresh["FetchUpdateLocal
fresh data from API\nUpdate local cache (Phase 2)"]
CheckNetworkViewUpdateLocal -->| UpdateUI["No"|Update ShowCached["Show cached data\UI\n(Phase 2) or empty state (Phase 1)"]
InitiatePayment -->|"Success"| ShowConfirmation["Show payment confirmation"]
InitiatePayment -->|"Failure"| ShowPaymentError["Show payment error\n(retry option)optimistic)"]
style ShowErrorUpdateUI fill:#f8d7da#d4edda
style ShowPaymentError fill:#f8d7da
style ShowCachedQueueAction fill:#fff3cd
style ShowConfirmation fill:#d4edda
7. Testing Strategy for Offline Scenarios
| Test Type | Scope | Tool |
|---|---|---|
| Unit | Jest | |
| Unit | Conflict resolution logic | Jest |
| Integration | Jest + | |
| E2E | Full offline → reconnect flow |
Detox / Maestro |
| Manual | Network conditions simulator | Network Link Conditioner (iOS), tc (Android emulator) |
ManualE2E offline test scenarios:scenario:
- Open app online — verify data loads
- Enable airplane mode
on device AttemptPerformtocreate/update/deletesend money → verify error message appears (not crash)actionsAttemptVerifyQRoptimisticpaymentUI→ verify error message appearsupdatesNavigateVerifytoactionstransaction history → verify error or empty statequeued (Phaseinspect1)DB)- Disable airplane mode
- Verify
verifysyncappqueuerefreshesdrains - Verify server data
automaticallymatches local state
8. Storage Limits & Data Eviction Policy
Phase 1: No local storage to manage.
Phase 2 targets:
| Storage Type | Soft Limit | Hard Limit | Eviction Strategy |
|---|---|---|---|
| SQLite |
Evict |
||
| LRU eviction | |||
| Total app storage | Warn user, offer cleanup |
Low storage alert: When device storage < 200500 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 |
|---|---|---|
| Toast: " |
||
| Alert: " |
||
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | |||
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |