Back to Blog
Engineering

Going Offline-First: Why It's Harder Than It Looks

Building a task app that truly works offline — and syncs without conflicts — is one of the hardest architectural problems in web development. Here's how KnotDo approaches it.

ETEngineering Team
May 8, 2026
9 min read

Most web apps treat offline support as an afterthought — a nice-to-have that shows up as a cached page when connectivity drops. KnotDo is built the opposite way: local-first, with the network as a sync layer rather than a dependency.

This is a harder architecture to build and maintain than server-dependent apps. But for a task manager, it's the right architecture — tasks are exactly the kind of data that should be available and editable anywhere, regardless of network status.

Here's how it works, and why the tradeoffs are worth it.

What "Offline-First" Actually Means

There's a spectrum worth naming. "Offline-capable" means the app doesn't break without internet — it might show cached data, but you can't do anything meaningful. "Offline-tolerant" means some actions queue up for later. "Offline-first" means the local data store is the source of truth, and the network is secondary.

In an offline-first architecture, every read and write happens against the local database first. The UI never waits for a network response to render or update. Syncing to the server happens asynchronously, in the background, when a connection is available. The user's experience is identical whether they're online or offline — because from the app's perspective, they always are.

IndexedDB and Dexie.js

IndexedDB is the browser's built-in persistent database. It's asynchronous, transactional, and can store structured data including large blobs. It survives browser restarts, stores gigabytes of data, and is available in all modern browsers including mobile.

The raw IndexedDB API is notoriously verbose and callback-heavy. Dexie.js wraps it with a clean, Promise-based interface and adds compound indexes, versioned schema migrations, and observable queries via the liveQuery API. It's well-maintained, has solid TypeScript support, and handles IndexedDB schema upgrade edge cases that would otherwise take significant effort to get right.

KnotDo's Dexie schema mirrors the server-side Postgres schema with additions for sync state. Every table has a syncedAt timestamp and a pendingSync boolean. When a record is created or modified offline, pendingSync is set to true. The sync engine picks up anything with pendingSync: true and pushes it to the server when a connection is available. On successful sync, pendingSync is cleared and syncedAt is updated.

The Service Worker Layer

Service workers are JavaScript files that run in a separate browser thread, acting as a programmable proxy between the app and the network. They're what makes PWAs feel like native apps: they cache app assets, intercept network requests, handle background sync, and receive push notifications even when the tab is closed.

KnotDo's service worker handles two jobs. First, it caches the app shell — HTML, CSS, JavaScript, and static assets — so the app loads instantly regardless of connectivity. Static assets use a cache-first strategy. API calls use network-first, falling back to cached responses when offline.

Second, it registers a Background Sync event. When the user makes changes offline, those changes persist locally. When the device reconnects — even if the tab was closed in the interim — the service worker fires a sync event and the pending changes are pushed to the server automatically. The user doesn't need to reopen the app or take any action.

Browser support for Background Sync varies (Firefox doesn't support it as of 2026). For unsupported browsers, KnotDo falls back to polling on app focus. Less elegant, but it achieves the same result for the majority of offline scenarios.

Sync Engine and Conflict Resolution

Offline-first systems face a problem that online-only apps never encounter: what happens when the same record is modified in two places while disconnected?

Imagine a task titled "Write Q2 report." One user renames it offline to "Write Q2 summary." While they're offline, another user renames the same task to "Write Q2 draft." When the offline device reconnects, both edits exist with different values. Which one wins?

Common approaches to conflict resolution include:

  • Last-Write-Wins (LWW): The most recent timestamp wins. Simple and deterministic, but an offline user who worked for hours will always lose to anyone who made changes while connected, even if the offline work happened "later" conceptually.
  • CRDTs (Conflict-free Replicated Data Types): Mathematical data structures that merge deterministically without conflicts. Automerge and Yjs are mature implementations. Powerful, but they add significant bundle size and operational complexity.
  • Server-wins: Server state always takes precedence. Offline changes that conflict are discarded. Simple to implement, poor user experience.
  • Field-level LWW: Rather than record-level timestamps, each field carries its own timestamp. Two users editing different fields of the same record simultaneously both keep their changes. Conflicts only occur when two edits target the exact same field at the exact same time.

KnotDo uses field-level Last-Write-Wins. Every task field — title, notes, due date, priority, assignee, status — carries its own updatedAt timestamp. When syncing, the engine compares field-level timestamps rather than record-level ones. In practice, simultaneous edits to the same specific field on the same task are rare. When they do occur, the later timestamp wins and a sync log records what changed.

CRDT-based sync is on the roadmap for high-collaboration scenarios where field-level LWW isn't sufficient.

Multi-Tab Sync via BroadcastChannel

A less obvious problem: if the user has KnotDo open in two browser tabs, creating a task in Tab A doesn't automatically notify Tab B. Both tabs share the same IndexedDB, but there's no built-in notification mechanism between browser contexts.

The BroadcastChannel API solves this. It provides a simple message bus between scripts in different tabs or workers on the same origin. When the sync engine completes a write in any context, it posts a message to the knotdo-sync channel. Any open tab listening to that channel invalidates its Dexie liveQuery subscriptions and re-renders with fresh local data. The result: changes made in one tab appear in other tabs within milliseconds, with no server roundtrip required.

Common Offline-First Implementation Challenges

These are well-documented challenges in building offline-first web apps — not unique to KnotDo, but worth addressing explicitly:

Sync frequency: Syncing on every keystroke fills the sync queue with thousands of tiny updates and creates UI flickering as local and remote state fight each other. Debounced syncing — 500ms after last edit for text fields, immediate for discrete state changes like task completion — eliminates both problems.

Schema migrations: When the app schema changes (a new field is added to the task table), existing IndexedDB instances don't know about it. Dexie's versioned migration system handles this: bump the version number, provide an upgrade function. The key is running schema validation on startup and applying pending migrations before the app renders anything — otherwise users returning after a long absence can encounter stale schemas.

Debugging: Bugs in offline-first apps can originate from the local database, the sync engine, the service worker, or the server — or some combination. Clear visibility into the current sync queue, pending operations, and last sync timestamp for each data type is essential for diagnosing issues quickly.

Is Offline-First Worth the Complexity?

For a task manager, yes. The performance benefits alone justify it — an app reading from a local database is faster than any server-dependent app, regardless of network speed. The reliability benefits are the real reason to do it: tasks are available in airports, basements, spotty conference Wi-Fi, and during server maintenance windows. Work created offline doesn't disappear.

The tradeoff is real: offline-first architecture adds development complexity and introduces a class of sync bugs that are harder to reproduce and debug than straightforward server-side errors. For apps where offline access isn't a meaningful requirement, the added complexity may not be worth it.

For KnotDo, offline-first is a core product requirement. It's why the app exists as a distinct alternative to server-dependent task managers.