Offline-first data layer

Eremite.js

Your app, in quiet retreat from the network.

Eremite.js is a zero-dependency data layer for apps that speak to any REST backend. Reads are kept locally in IndexedDB and paint instantly on the next visit. Writes take effect in the UI at once, wait in a durable outbox, and are delivered to your API in order and exactly once. Like a hermit, your app answers to no one: if fetch can reach your server, you can go offline-first.

  • · zero runtime deps
  • · exactly-once writes
  • · multi-tab safe
  • · MIT licensed
store.ts
import { collection, createStore } from '@eremitejs/core'

const store = createStore({
  name: 'app',
  collections: { todos: collection<Todo>() },
  mutators: {
    addTodo (tx, input: Todo) {
      tx.todos.set(input.id, input)
    }
  },
  push: {
    async addTodo ({ input, idempotencyKey }) {
      await api.post('/todos', input, { idempotencyKey })
    }
  }
})

// visible now, queued offline, pushed exactly once
store.mutate.addTodo({
  id: store.id(),
  title: 'Ship it'
})

entities awaiting sync carry $pending

The rule of the order

Four rules, kept strictly

Eremite never mixes confirmed data with pending changes. Everything else follows from that.

The outbox & the optimistic rebase

Confirmed "base state" is kept apart from your pending changes. The screen always shows base state plus pending operations replayed in order, so a rollback can never corrupt data. Anything still waiting for the server carries a $pending mark.

Any backend, nothing to install

No sync engine, no bespoke protocol, no server of its own to run. If you can reach your API with fetch, you can go offline-first. There are zero runtime dependencies: the browser already ships IndexedDB, Web Locks, BroadcastChannel and crypto.

Server-assigned IDs, kept honest

Placeholder refs stand in for IDs your server has not assigned yet, across relations, reloads and retries. Operations that depend on one wait their turn automatically. If the create is rejected, everything that depended on it rolls back with it and shows up as a single group of conflicts to retry or discard.

Safe across every tab

Open tabs elect a leader through Web Locks, so an action is never submitted twice. Writes reach your API in order and exactly once, even if the tab was closed halfway through.

Bring your own backend.

Eremite.js lives entirely in the browser. It withdraws from the network when it has to and catches up when it can, while your API stays a plain REST API. The core is framework-agnostic, with official Vue 3 and React bindings.

Take up the habit

Two packages, and you are in retreat

Install the core, then add the binding for your framework. Everything is MIT licensed and ships with no runtime dependencies.

@eremitejs/core@eremitejs/vue@eremitejs/react
terminal
# the self-sufficient core
npm install @eremitejs/core

# then your framework binding
npm install @eremitejs/vue   # or @eremitejs/react