Overview
PassAgent works offline as a progressive web app (PWA). When you lose connectivity, your vault remains accessible through a multi-layer caching architecture: a Service Worker intercepts network requests and serves cached responses, IndexedDB stores vault entries locally, and a sync manager reconciles changes when you come back online.Caching strategies
The Service Worker (public/sw.js) routes requests through four caching strategies based on request type:
| Request type | Strategy | Cache name | Max entries |
|---|---|---|---|
Sync API (/api/passwords/sync) | Network-first | passagent-sync-v1 | 10 |
Static assets (/_next/static/) | Cache-first | passagent-app-v1 | 200 |
| Navigation (HTML pages) | Network-first with offline fallback | passagent-app-v1 | 200 |
| Images, fonts, icons | Stale-while-revalidate | passagent-app-v1 | 200 |
- Network-first (sync)
- Cache-first (static)
- Network-first with fallback (HTML)
- Stale-while-revalidate (media)
Fetches from the network and caches successful responses. If the network fails, returns the cached response. If no cached data exists, returns a
503 JSON error with "error": "offline".App shell pre-caching
On Service Worker install, the following URLs are eagerly cached so the app shell loads on a full offline refresh:/dashboard/offline
Pre-caching uses
Promise.allSettled so the install event succeeds even if individual URLs fail to fetch.Cache lifecycle
Install
Pre-caches the app shell URLs into
passagent-app-v1. Calls skipWaiting() to activate immediately.Activate
Deletes any old caches with the
passagent- prefix that are not in the current set (passagent-app-v1, passagent-sync-v1). Calls clients.claim() to take control of all open tabs.Runtime
Intercepts same-origin GET requests and routes them through the appropriate caching strategy. Non-GET requests and cross-origin requests pass through unmodified.
IndexedDB storage
The client-side sync system uses an IndexedDB database for persistent local storage:| Database | Version | Purpose |
|---|---|---|
passagent-sync | 1 | Vault entries, pending mutations, sync metadata |
| Store | Contents |
|---|---|
entries | Cached vault entries keyed by (userId, id) |
pending | Queued offline mutations (create, update, delete) |
metadata | Sync cursors and timestamps per user |
Sync manager
TheSyncManager singleton coordinates data flow between IndexedDB and the server. It runs in the main thread and exposes event listeners for status changes, entry updates, and conflict notifications.
Delta sync
Pull changes
Sends
GET /api/passwords/sync?since=<cursor> to fetch entries modified since the last sync. On the first sync (no cursor), fetches all entries.Merge
For delta responses, each change is applied individually: deleted entries are removed from IndexedDB, new and updated entries are upserted. For full sync responses (
fullSyncRequired: true), the local cache is cleared and replaced entirely.Pushing mutations
When you create, update, or delete a vault entry while offline, the mutation is queued in thepending store with these fields:
| Field | Description |
|---|---|
action | create, update, or delete |
entryId | The entry ID |
data | The encrypted entry payload (for create/update) |
clientUpdatedAt | Timestamp when the mutation was made |
userId | The authenticated user ID |
pushPendingMutations() sends all queued mutations to POST /api/passwords/sync in a single batch.
Conflict resolution
The sync system uses a server-wins strategy:How conflicts are resolved
How conflicts are resolved
- Each mutation includes a
clientUpdatedAttimestamp. - The server compares it against the entry’s
updated_at. - If
server.updated_at > clientUpdatedAt, the server version takes precedence and the result status isconflict_server_wins. - The local IndexedDB cache is updated with the server’s version of the entry.
- The
onConflictlistener fires with the affected entry ID so the UI can notify you. - Successfully synced mutations (both
appliedandconflict_server_wins) are removed from the pending queue.
Background sync
The Service Worker registers for the Background Sync API with the tagvault-sync. When connectivity is restored, the browser wakes the Service Worker, which runs pushPendingMutations():
Fetch CSRF token
Calls
GET /api/auth/csrf-token to obtain a fresh CSRF token. The sync POST requires CSRF validation.Push mutations
Sends
POST /api/passwords/sync with the x-csrf-token header and a JSON body containing the queued mutations.Periodic sync
The sync manager runs a periodic sync loop while the app is active:| Behavior | Detail |
|---|---|
| Interval | 30 seconds |
| Skipped when | Tab is hidden (document.visibilityState === "hidden") or offline (!navigator.onLine) |
| Triggered on | online event (browser regains connectivity) |
| Triggered on | visibilitychange event (tab becomes visible) |
| Sequence | Push pending mutations first, then pull delta changes |
Cache management messages
The app can send messages to the Service Worker to manage caches:| Message type | Effect |
|---|---|
CLEAR_SYNC_CACHE | Deletes the passagent-sync-v1 cache entirely |