In this post, I want to talk about some new challenges I’ve encountered while rebuilding my project as a Progressive Web App (PWA). As I mentioned in my last post, the PWA paradigm is a big reason I chose not to use Next.js. But beyond framework choice, building with an “offline-first” mindset has introduced a number of new technical problems I hadn’t fully anticipated.

Here are some of the issues I’ve been working through over the past few weeks:


How do you persist data when you can’t guarantee a connection to a server?

To solve this, I’m using the browser’s IndexedDB API via a wrapper library called Dexie. Dexie allows me to interact with the local database using a clean, promise-based API—similar to working with a backend ORM.

Using IndexedDB lets me persist CRUD operations locally, which is essential for offline functionality. However, IndexedDB is a NoSQL store, while my backend is built on Drizzle with PostgreSQL—so the data models are structurally very different. That leads to the next challenge…


What happens when the client has updates the server doesn’t? What if those states conflict?

Here’s the architecture I’ve landed on so far:

  • IndexedDB will act as the source of truth for the UI and the Server will be the source of truth for IndexedDB. So, the UI won’t get any of it’s data through typical REST endpoints, at least not directly.
  • I’m building programming interfaces on both the frontend and backend to synchronize state.
  • When the user mutates data locally, it stages a “mutation”—a command object containing an action type and any necessary payload.
  • Once online, the app will batch and push these mutations to the server.
  • The server will process the mutation queue and return an updated payload to reconcile local state.
  • Some mutations (e.g., by admin users) may be applied automatically. Others may require approval and remain in a pending state until confirmed server-side.

How do you handle auth if data lives on the client?

This part is tricky. All server interactions will require a valid HTTP-only JWT, meaning mutations are protected at the network layer. But since IndexedDB is a local store, it’s inherently more exposed—especially in the context of XSS attacks.

Here’s how I’m mitigating the risks:

  • Strict input validation and sanitization across the board, to prevent XSS.
  • No sensitive data is stored in IndexedDB—only what’s needed to render the UI.
  • All sensitive operations require a network connection and valid auth.
  • Staged mutations follow a strict schema, enforced by Zod. Each mutation has a predefined action and a type-safe payload. The frontend has no awareness of table names or low-level DB structures. Mutations don’t allow custom operations (e.g., DROP, bulk inserts), and their scope is strictly defined by backend action handlers.

I’ll share more soon about how I’m handling sync failures, versioning, and conflict resolution. The goal is to give users as seamless an experience as possible—even if they’re stuck in the middle of nowhere with no signal.