Offline first strategy
Skill almasumdev/awesome-mobile-agent-skills/.github/skills/offline/offline-first-strategy
Designing a mobile app as offline-first - caching tiers, authoritative sources, read and write paths, and the boundaries between device and server. Use when a feature needs to work without connectivity or when diagnosing stale/inconsistent data.From its SKILL.md
npx -y skills add almasumdev/awesome-mobile-agent-skills --skill offline-first-strategyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.0 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Offline-First Strategy
Instructions
Mobile devices are flaky by nature. Treat the network as optional. Offline-first apps feel fast, survive tunnels and airplane mode, and reduce server load. The trick is picking the authoritative source per data kind and building a predictable read/write path around it.
1. Tiers of Cache
| Tier | Where | Lifespan | Example |
|---|---|---|---|
| L0: in-memory | app process | foreground session | current screen view model |
| L1: on-disk structured | SQLite, Realm, Core Data, Room, Drift, SQLDelight | persistent | lists, entities, search indices |
| L2: on-disk blob | file cache directory | purgeable | images, videos, exports |
| L3: secure | Keychain / Keystore | persistent, encrypted | tokens, keys |
| Remote | server | authoritative for shared data | transactional writes |
Pick tiers explicitly per data type; do not let a JSON in SharedPreferences grow into a database.
2. Authoritative Source Rule
For every data kind, declare one authoritative source:
- User-owned local data (notes draft, reading position, settings): device is authoritative, server is backup.
- Shared collaborative data (chat messages, shared docs): server is authoritative, device is cache.
- Reference data (catalog, pricing): server authoritative, device caches with TTL.
- Secrets (tokens): keychain authoritative, memory is ephemeral.
Writing this down prevents "whose value wins" debates during conflict resolution.
3. Read Path
UI -> Repository.read(id) -> emits Loading -> emits Cached (fast) -> refresh if stale -> emits Fresh
- Return cached data immediately on subscribe.
- Fire a revalidation in parallel if the cache is stale.
- Emit new state when fresh data arrives. Never block the UI on the network when a cache exists.
// Kotlin (Flow)
fun observeArticle(id: String): Flow<Resource<Article>> = flow {
val cached = dao.get(id)
if (cached != null) emit(Resource.Success(cached))
runCatching { api.article(id) }
.onSuccess { fresh -> dao.upsert(fresh); emit(Resource.Success(fresh)) }
.onFailure { if (cached == null) emit(Resource.Error(it)) }
}
// Swift
func observeArticle(id: String) -> AsyncStream<Resource<Article>> {
AsyncStream { cont in
Task {
if let cached = try? await store.get(id) { cont.yield(.success(cached)) }
do {
let fresh = try await api.article(id)
try await store.upsert(fresh)
cont.yield(.success(fresh))
} catch {
// keep cached view; surface error only if nothing is cached
}
cont.finish()
}
}
}
// Dart
Stream<Resource<Article>> observeArticle(String id) async* {
final cached = await dao.get(id);
if (cached != null) yield Resource.success(cached);
try {
final fresh = await api.article(id);
await dao.upsert(fresh);
yield Resource.success(fresh);
} catch (e) {
if (cached == null) yield Resource.error(e);
}
}
4. Write Path
Writes should never block the UI on the network.
- Write to the local store immediately with an
isPendingflag. - Render the UI from the local store; the change appears instantly.
- Enqueue the write to an outbox.
- A sync worker drains the outbox with retry/backoff.
- On ack, clear the pending flag; on permanent failure, mark conflict and surface it.
This is the outbox pattern and it is the single biggest win for perceived performance.
5. Cache Invalidation
Choose an invalidation strategy per data kind:
- TTL: staleAfter + hardExpireAfter. Simplest.
- ETag / Last-Modified: server-led freshness; supports 304 responses.
- Subscription / push: server tells you when to invalidate (websocket, SSE, silent push).
- Versioned bundles: for reference data, bump a version key and purge.
Document the policy per entity.
6. Network Awareness
- Detect connectivity changes but do not use them as the only signal. A reachable API may still fail.
- On regaining connectivity, drain the outbox and revalidate visible screens only (not the whole cache).
- Surface a subtle offline indicator; avoid full-screen blockers unless the user explicitly chose an online-only feature.
7. Storage Budgets
- Set a cap (for example 200 MB) and evict LRU when exceeded.
- Keep sensitive data out of L2 blob caches.
- Never store unbounded history on-device.
- Provide a "clear cache" action somewhere in settings for support.
8. Testing Offline
- Unit test the repository with a fake remote that can be in three states: ok, slow, failing.
- UI tests toggle airplane mode via
adborxcrun simctl status_bar override. - Seed a realistic cache on device before running offline flows.
- Track a CI job that runs the critical-path suite with network disabled.
9. Anti-Patterns
- Writing to the network first and then to the cache. The UI is sluggish and inconsistent.
- Storing derived views on disk (search results, sorted lists). Store facts; derive on read.
- Global caches with no eviction policy.
- Silent failures when offline. Surface the pending state in the UI.
- Shipping features that only work online without saying so.
Checklist
- Each data kind has a named authoritative source.
- Reads return cache first, then revalidate.
- Writes use an outbox with retry/backoff and a pending UI state.
- Cache invalidation policy is documented per entity.
- Secrets are in keychain/keystore, never in the main database.
- Storage has a cap and an eviction policy.
- Connectivity changes drive revalidation, not feature gating.
- Critical paths have offline integration tests.
- Offline indicator is present and non-blocking.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.