npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anfenn/dync

v1.1.3

Published

Write once, run IndexedDB & SQLite with sync anywhere - React, React Native, Expo, Capacitor, Electron & Node.js

Readme

Dync

npm version

A complete Typescript offline-first data layer with optional sync engine for any local storage (IndexedDB, SQLite, etc.), and any backend (Restful, GraphQL, Supabase, etc.) in a Website, PWA, CapacitorJs, React Native, or Electron app.

Start with a Website or PWA using IndexedDB, sync with your existing REST API, and later ship native apps with encrypted SQLite - with no code changes - for free!

Why Dync?

  1. Target IndexedDB as a PWA, and SQLite in the AppStores, with no code changes and native storage performance

  2. Frictionless upgrade path from an offline-first PWA targeting IndexedDB, to when:

    A) Substring search is required on many records:

    • IndexedDB doesn't support this so will do a full table scan in the JS VM, which is both slow and will spike memory

    AND/OR

    B) Encryption is required:

    • Browsers can't store the encryption key securely
    • A user's password could be used as the encryption key instead, but if the app allows biometric login, then there will be no password during those logins to decrypt the database

    ... so you can simply add CapacitorJs or move to React Native which have sqlite & secure enclave storage, and only change the adapter Dync uses

  3. Completely free and open source

See first-hand in this fully working example: examples/react-capacitor

And see how Dync compares to the alternatives below.

Goals

  • Persist SQL or NoSQL data locally and sync some or all tables to a backend

  • Storage agnostic. Comes with Memory, IndexedDB and SQLite adapters (for CapacitorJs & React Native), and extendable with your own custom adapters

  • Lazy loaded data keeps it in native storage, allowing low memory and fast app response, even with >100K records

  • Fast React Native SQLite access via JSI

  • Single collection based api for both SQLite & IndexedDB, plus query() escape hatch for native storage api e.g.:

    • db.myTable.add() | .update() | .where('myField').equals(42).first()
    • db.query() is only intended to retrieve records, any mutations will be ignored by the sync engine:
      db.query(async (ctx) => {
          if (ctx instanceof DexieQueryContext) {
              return await ctx.table('items').where('value').startsWithIgnoreCase('dexie').toArray();
          } else if (ctx instanceof SQLiteQueryContext) {
              return await ctx.queryRows('SELECT * FROM items WHERE value LIKE ?', ['sqlite%']);
          }
      });
  • Sync some or all tables with any backend in 2 ways:

    • Option 1: Map remote api CRUD urls to a local table:

      const db = new Dync({
          ...,
          sync: {
              // Only add an entry here for tables that should be synced
              // Pseudocode here, see examples for working code
              items: {
                  add: (item) => fetch('/api/items'),
                  update: (id, changes) => fetch(`/api/items/${id}`),
                  remove: (id) => fetch(`/api/items/${id}`),
                  list: (since) => fetch(`/api/items?since=${since}`),
                  // Optional: Delay calling this endpoint during a pull if slow changing data, to reduce server load
                  listExtraIntervalMs: 7 * 24 * 60 * 60 * 1000, // 1 week
              },
          },
      });
    • Option 2: Batch sync to remote /push & /pull endpoints:

      const db = new Dync({
          ...,
          sync: {
              syncTables: ['items'], // Only add tables to this array that should be synced
              push: async (changes) => {
                  const res = await fetch('/api/sync/push', {
                      method: 'POST',
                      headers: { 'Content-Type': 'application/json' },
                      body: JSON.stringify(changes),
                  });
                  return res.json();
              },
              pull: async (since) => {
                  const params = new URLSearchParams(Object.entries(since).map(([table, date]) => [table, date.toISOString()]));
                  const res = await fetch(`/api/sync/pull?${params}`);
                  return res.json();
              },
          },
      });

      See examples/shared/api.ts for a fully documented example of these two options.

  • Full conflict resolution: local-wins, remote-wins or with try-shallow-merge the user can resolve with:

    const syncState = useSyncState(db);
    syncState.conflicts; // Record<localId, Conflict>
    db.sync.resolveConflict(localId, true);
  • Optimistic UI updates

  • Offline detection: syncState.apiError.isNetworkError

  • Optional first load data download before periodic sync is enabled

  • Missing remote record on update strategy: ignore | delete-local-record | insert-remote-record

  • Reactive updates when data changes via useLiveQuery() React hook:

    useLiveQuery(
        db,
        async () => {
            const items = await db.items.toArray(); // toArray() executes the query
            setTodos(items);
        },
        [], // Re-run when variables change (None defined)
        ['items'], // Re-run when tables change
    );
  • SQLite schema migration:

    db.version(1).stores({ items: { columns: { name: { type: 'TEXT' } } } });
    db.version(2)
        .stores({
            items: {
                columns: {
                    name: { type: 'TEXT' },
                    priority: { type: 'INTEGER' },
                },
            },
        })
        .sqlite({
            up: async (ctx) => {
                await ctx.execute('ALTER TABLE "items" ADD COLUMN "priority" INTEGER DEFAULT 0');
            },
            down: async (ctx) => {
                await ctx.run('UPDATE "items" SET "priority" = NULL');
            },
        });
  • "It just works" philosophy

  • Modern and always free (MIT)

Non-Goals

  • Full IndexedDB & SQL unified query language:
    • Using IndexedDB functions or raw SQL will always be more expressive independently
    • When required, best performance will always come from native api
    • No need to learn another api when you might only need one storage type
    • Would greatly increase complexity of this library

Hasn't this already been done?

Many times, with varying degrees of functionality, compatibility, and cost.

Dync aims to be a performant, multi-platform, modern and always free alternative.

This is how Dync compares to other multi-platform sync engines:

Legend:

  • SQLite: Used natively by an installed Capacitor, React Native, Electron or Node app
  • WA-SQLite: Official SQLite compiled to WebAssembly that runs in the browser. Persists to IndexedDB or OPFS depending on VFS configuration (see SQLite vs WA-SQLite)

| Library | Installed Components | IndexedDB | SQLite | WA-SQLite | Any Backend | CRUD Sync | Batch Sync | Conflict Resolution | Platforms | Notes | | --------------------------------------------------------------------- | -------------------- | --------- | ---------- | ---------- | ------------------------ | --------- | ---------- | ------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Dync | Client | ✅ | ✅ Free | ✅ Free | ✅ | ✅ | ✅ | ✅ | Web, Capacitor, RN, Electron, Node | | | RxDB | Client | ✅ | 💰 Premium | 💰 Premium | ✅ | ❌ | ✅ | ✅ | Web, Capacitor, RN, Electron, Node | | | WatermelonDB | Client | ✅ | ✅ Free | ❌ | ✅ | ❌ | ✅ | ❌ | Web, RN, Node | ⚠️ Not Vite compatible⚠️ Uses legacy JS proposals | | Legend State | Client | ✅ | ✅ Free | ❌ | ✅ | ✅ | ❌ | ❌ | Web, RN | ⚠️ Data loss bugs as of 01/01/2026⚠️ Confusing observables based React api | | SignalDB | Client | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | Web | | | TanStack DB | Client | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | Web, RN | In-memory onlyIntegrates with RxDB/PowerSync/Electric for persistence | | Dexie.js | Client | ✅ | ❌ | ❌ | ❌ Dexie cloud only | ❌ | ✅ | ❌ | Web, Capacitor, RN, Electron | | | PouchDB | Client | ✅ | ❌ | ❌ | ❌ CouchDB only | ❌ | ✅ | ✅ | Web, Electron, Node | | | TinyBase | Client | ✅ | ✅ Free | ✅ Free | ❌ Client-to-client only | ❌ | ✅ | ✅ | Web, RN, Electron, Node | | | ElectricSQL | Client & Server | ❌ | ✅ Free | ✅ Free | ❌ Postgres only | ❌ | ✅ | ✅ | Web, RN, Electron, Node | | | PowerSync | Client & Server | ❌ | ✅ Free | ✅ Free | ❌ PowerSync server only | ❌ | ✅ | ✅ | Web, Capacitor, RN, Electron, Node | | | InstantDB | Client | ✅ | ❌ | ❌ | ❌ InstantDB cloud only | ❌ | ✅ | ✅ | Web, RN | | | Firebase/Firestore | Client | ✅ | ❌ | ❌ | ❌ Firebase cloud only | ❌ | ✅ | ✅ | Web, iOS, Android, RN, Flutter, Node | |

Examples

Both are fully commented and can be run in the browser and natively on iOS/Android without code change:

  1. React + Capacitor: examples/react-capacitor - IndexedDB in the browser, SQLite natively
  2. React Native + Expo SQLite: examples/react-native-expo-sqlite - WA-SQLite in the browser, SQLite natively

Design

Server Requirements

Your server records must have these fields. If it does but they're named differently, rename them in your client's api.ts using the included changeKeysFrom() & changeKeysTo() helpers:

| Field | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | Unique identifier (any datatype). Can be assigned by client or server. | | updated_at | Server-assigned millisecond timestamp (e.g. via db trigger or API layer). The client never sends this as client clocks are unreliable. Ensure precision doesn't exceed milliseconds (like PostgreSQL's microsecond timestamptz), otherwise updates may be ignored. | | deleted | Boolean for soft deletes. Allows other clients to sync deletions to their local store. |

Client Records

Dync auto-injects these fields into your local table schema:

| Field | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | _localId | Stable local identifier, never sent to the server. Ideal for React keys. Auto-generated UUID, but can be set manually with any unique value. | | id | Unique identifier (any datatype). Can be assigned by client or server. | | updated_at | Assigned from the server's updated_at after sync. You may set it optimistically, but it's always overwritten on sync. |

Note: deleted doesn't exist on the client, as it's removed during sync.

SQLite vs WA-SQLite

SQLite runs natively in Capacitor, React Native, Electron, and Node apps via platform-specific drivers.

WA-SQLite is SQLite compiled to WebAssembly, enabling SQL in the browser. It persists data to IndexedDB or OPFS depending on the Virtual File System (VFS) you choose.

When to use WA-SQLite

  • You need SQL queries in a web app (full-text search, complex joins, etc.)
  • Your dataset is too large for efficient IndexedDB queries
  • You want the same SQLite schema across web and native apps
  • You don't need encryption, as browsers can't securely store the encryption key
  • You're happy for a larger runtime memory footprint

WA-SQLite VFS Options

Choose a VFS based on your app's requirements. See WaSQLiteDriverOptions for configuration.

| VFS | Context | Multi-Tab | Durability | Performance | Best For | | ----------------------- | ------- | --------- | ---------- | ----------- | ------------------------------------- | | IDBBatchAtomicVFS | Any | ✅ | ✅ Full | Good | General use, maximum compatibility | | IDBMirrorVFS | Any | ✅ | ⚠️ Async | Fast | Small databases, performance critical | | OPFSCoopSyncVFS | Worker | ✅ | ✅ Full | Good | Multi-tab apps needing OPFS | | AccessHandlePoolVFS | Worker | ❌ | ✅ Full | Best | Single-tab apps, maximum performance |

Notes:

  • IDBBatchAtomicVFS (default) is recommended for most apps - works in main thread and has full durability
  • IDBMirrorVFS keeps data in memory and mirrors to IndexedDB asynchronously - fast but may lose recent writes on crash
  • OPFS VFS types require a Web Worker context and are not supported on Safari/iOS
import { WaSQLiteDriver, SQLiteAdapter } from '@anfenn/dync/wa-sqlite';

const driver = new WaSQLiteDriver('mydb', {
    vfs: 'IDBBatchAtomicVFS', // or 'IDBMirrorVFS', 'OPFSCoopSyncVFS', 'AccessHandlePoolVFS'
});
const adapter = new SQLiteAdapter(driver);

Community

PRs are welcome! pnpm is used as a package manager. Run pnpm install to install local dependencies. Thank you for contributing!