@flexstore/core
v0.3.9
Published
FlexStore sync engine — SQLite WASM local store, protocol transport, background scheduler
Maintainers
Readme
@flexstore/core
Framework-agnostic FlexStore sync engine for browsers.
Repository: github.com/flexstoresync/flexstore-core
Data flow
flowchart TB
subgraph App["Your UI"]
UI[React / Vue / Angular / vanilla]
end
subgraph Core["@flexstore/core"]
CLIENT[SyncClient]
SCHED[Scheduler]
TRANS[Transport]
CLIENT --> SCHED
SCHED --> TRANS
end
subgraph Local["Browser local store (default)"]
IDB[(IndexedDB)]
end
subgraph Remote["flexstoresync server"]
API[Sync API]
LEDGER[(Ledger)]
API <--> LEDGER
end
UI <-->|create / query / subscribe| CLIENT
CLIENT <-->|upsert / pending / cursors| IDB
TRANS <-->|push / pull / check-deltas| APIWrite path (offline-first):
sequenceDiagram
participant UI as App UI
participant SC as SyncClient
participant DB as IndexedDbStore
participant IDB as IndexedDB
participant API as Sync API
UI->>SC: create / update / remove
SC->>DB: upsert row + sync_status
DB->>IDB: persist rows
SC-->>UI: immediate local read
Note over SC,API: background when online
SC->>API: check-deltas → push → pull
API-->>SC: remote changes
SC->>DB: apply pulled rows
SC-->>UI: subscribe callbacknpm install @flexstore/coreStore adapters
| Adapter | Export | When |
| ------- | ------ | ---- |
| IndexedDB (default) | IndexedDbStore | Persistent browser storage — no WASM setup |
| SQLite WASM (opt-in) | SqliteStore | SQL surface when you need it later |
import { createSyncClient, SqliteStore } from '@flexstore/core';
createSyncClient({
...config,
store: new SqliteStore(['todos'], 'flexstore-sqlite'),
});Implement FlexStoreAdapter (see index.d.ts) for React Native, Node, etc.
Local-store schema migrations (IndexedDbStore)
The default IndexedDbStore maps each ResourceDefinition to its own
IndexedDB object store. Object stores can only be created during the
onupgradeneeded
phase, which fires only when the database version increases.
When you add a new resource to your registry on an already-installed browser:
- The JS bundle still knows the new resource name (the SDK passes it to
IndexedDbStore). - The physical IndexedDB database still has only the object stores from the previous version.
indexedDB.open(name, version)against an existing DB does not fireonupgradeneededifversionis unchanged, so the new object store is never created.- The first
useQuery('newResource')orclient.create('newResource', …)throws:DOMException: Failed to execute 'transaction' on 'IDBDatabase': one of the specified object stores was not found.
Action. Bump the IndexedDB schema version whenever the resource registry grows:
- App-owned (recommended):
src/sync/local-schema.ts+idbSchemaVersionin sync config. - Core default:
DEFAULT_IDB_SCHEMA_VERSIONinsrc/store/idb.jsfor reference apps.
Custom per-version hooks: pass idbMigrations: [{ version, description, up(db, ctx) }] on sync config.
// packages/core/src/store/idb.js
export const DEFAULT_IDB_SCHEMA_VERSION = 3;The onupgradeneeded handler is idempotent — it calls createObjectStore
only for stores that are missing — so existing data in older stores is preserved
across the upgrade.
// src/store/idb.js
const IDB_SCHEMA_VERSION = 2; // bump when adding new resourcesThe SqliteStore adapter is unaffected: it uses CREATE TABLE IF NOT EXISTS,
so adding a new resource just adds the table on next init.
Workaround for affected users today. Open DevTools → Application → IndexedDB
→ delete the flexstore-idb:<dbName> database, then reload. The next session
re-creates the database with all current resources.
Quick start
import { createSyncClient, defineResource, resourceRegistry } from '@flexstore/core';
const todos = defineResource({
name: 'todos',
attributes: { title: 'string', done: 'boolean' },
});
const client = createSyncClient({
baseUrl: 'http://localhost:8088',
apiKey: 'my-key',
tenantId: 'my-tenant',
resources: resourceRegistry(todos),
// store: default IndexedDbStore (persistent)
});
await client.start();
await client.create('todos', { title: 'Buy milk', done: false });
const open = await client.query('todos', { done: false });Pub/sub + fallback polling
When pubsubUrl is set, the client subscribes to SSE sync hints for near-real-time pulls. A background check-deltas timer still runs as a safety net:
| Mode | Config | Default |
| ---- | ------ | ------- |
| Pub/sub connected | pubsubFallbackPollMs / pollIntervalConnectedMs | 60s |
| Pub/sub down / off | pollIntervalDisconnectedMs | 4s |
createSyncClient({
baseUrl: 'http://localhost:8088',
pubsubUrl: 'http://localhost:8090',
pubsubFallbackPollMs: 60_000, // or 0 for hints-only while connected
pollIntervalDisconnectedMs: 4_000,
// ...
});useSyncStatus() / client.status expose pollIntervalMs (active timer) and pubsubFallbackPollMs (configured fallback).
Automatic background sync on writes
You do not need to call syncNow() after a mutation. Every create / update / remove writes locally (instant, UI never blocks) and schedules a debounced background sync. Rapid successive writes — e.g. a checkout inserting many line and payment rows — coalesce into a single sync. Scheduling respects paused / blocked, so it stays offline-first.
Tune the debounce with syncDebounceMs (default 300ms; 0 = sync immediately on each write):
createSyncClient({
baseUrl: 'http://localhost:8088',
syncDebounceMs: 300, // coalesce bursts of writes into one sync
// ...
});Reserve explicit syncNow() for user-initiated "refresh now" actions (e.g. a manual Refresh button that forces an immediate pull).
Framework wrappers
| Package | Repo |
| ------- | ---- |
| @flexstore/react | flexstore-react |
| @flexstore/vue | flexstore-vue (composables) |
| Angular | Use createSyncClient + inject a singleton service (see packages/angular in monorepo) |
Related
| Repo | Role | | ---- | ---- | | flexstore-self-host | Docker sync server | | flexstore | Monorepo + design docs |
License
MIT — see LICENSE.
