Skip to content

Drakkar-Software/Anchor

Repository files navigation

Anchor

Anchor

Type-safe Zustand stores auto-generated from your Supabase schema. Offline-first, realtime, with optimistic updates.

Features

  • Auto-generated, type-safe stores from Supabase Database types — filters, mutations, and hooks fully typed
  • Optimistic mutations with automatic rollback, validation, and conflict resolution
  • Offline-first with persistent queue, coalescing, dependency tracking, and auto-flush on reconnect
  • Realtime & sync — Supabase subscriptions, cross-tab (BroadcastChannel), multi-device, incremental and selective sync
  • Caching — query cache strategy (replace/merge), cursor pagination, infinite scroll, cache TTL with stale-while-revalidate
  • Platform adapters — Web (localStorage/IndexedDB) and React Native (expo-sqlite/AsyncStorage/background sync)
  • Auth, RSC & Suspense — session-gated stores, RLS awareness, server prefetch, React Suspense
  • Resilience — retry with backoff, circuit breaker, rate limiter, encryption at rest, storage quota, schema versioning
  • Observability — sync status hooks, sync health metrics, conflict audit trail
  • Full Supabase coverage — Storage, Edge Functions, RPC, aggregation (client & server)

Installation

npm install @drakkar.software/anchor zustand @supabase/supabase-js
# Web adapters
npm install @drakkar.software/anchor-adapter-web
# React Native adapters
npm install @drakkar.software/anchor-adapter-react-native

Quick Start

1. Generate types from your Supabase schema

npx supabase gen types typescript --project-id $PROJECT_REF > database.types.ts

2. Create stores for all tables

import { createClient } from '@supabase/supabase-js'
import { createSupabaseStores } from '@drakkar.software/anchor'
import { LocalStorageAdapter, WebNetworkStatus } from '@drakkar.software/anchor-adapter-web'
import type { Database } from './database.types'

const supabase = createClient<Database>(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
)

export const stores = createSupabaseStores<Database>({
  supabase,
  tables: ['todos', 'profiles'],
  persistence: { adapter: new LocalStorageAdapter() },
  network: new WebNetworkStatus(),
  realtime: { enabled: true },
  devtools: process.env.NODE_ENV === 'development',
})

3. Use in React components

import { useQuery, useMutation, eq, isPending } from '@drakkar.software/anchor'

function TodoList() {
  const { data, isLoading } = useQuery(stores.todos, {
    filters: [eq('completed', false)],
  })
  const { insert, remove } = useMutation(stores.todos)

  if (isLoading) return <div>Loading...</div>

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>
          {todo.title}
          {isPending(todo) && <span> (saving...)</span>}
          <button onClick={() => remove(todo.id)}>Delete</button>
        </li>
      ))}
      <button onClick={() => insert({ title: 'New todo' })}>
        Add Todo
      </button>
    </ul>
  )
}

API Reference

Store Factories

createTableStore(options)

Creates a Zustand store for a single Supabase table.

const todosStore = createTableStore<Database, TodoRow, TodoInsert, TodoUpdate>({
  supabase,
  table: 'todos',
  primaryKey: 'id',                    // default: 'id'
  schema: 'public',                    // default: 'public'
  defaultSort: [{ column: 'created_at', ascending: false }],
  persistence: { adapter: new LocalStorageAdapter() },
  devtools: true,
  crossTab: { enabled: true },
  validate: {
    insert: (data) => data.title?.length > 0 ? true : ['Title required'],
    update: (data) => true,
  },
  extend: (set, get, store, supabase) => ({
    completedCount: () =>
      [...get().records.values()].filter(t => t.completed).length,
    toggleComplete: async (id: number) => {
      const current = get().records.get(id)
      if (current) await get().update(id, { completed: !current.completed })
    },
  }),
})

Note: realtime, conflict, network, and offlineQueue options require createSupabaseStores() which wires up the shared RealtimeManager and OfflineQueue. Use createSupabaseStores() for full-featured stores, or manually set up these features with RealtimeManager, bindRealtimeToStore, and OfflineQueue.

createSupabaseStores(options)

Creates typed stores for multiple tables at once.

const stores = createSupabaseStores<Database>({
  supabase,
  tables: ['todos', 'profiles', 'comments'],
  persistence: { adapter: new LocalStorageAdapter() },
  realtime: { enabled: true },
  tableOptions: {
    todos: { defaultSort: [{ column: 'created_at', ascending: false }] },
    profiles: { realtime: { enabled: false } },
  },
})

// Fully typed:
stores.todos.getState().insert({ title: 'Buy milk' })
stores.profiles.getState().fetch()
stores._destroy() // Clean up all subscriptions

createViewStore(options)

Creates a read-only store for database views. Mutations throw an error.

const statsStore = createViewStore<Database, StatsRow>({
  supabase,
  view: 'dashboard_stats',
})

const stats = await statsStore.getState().fetch()
// statsStore.getState().insert({}) // Throws: "Cannot mutate view"

Store Actions

Every table store provides these actions:

Action Description
fetch(options?) Fetch rows from Supabase with filters/sort/pagination
fetchOne(id) Fetch a single row by primary key
refetch() Re-run the last fetch
insert(row) Insert a row (optimistic)
insertMany(rows) Batch insert (single HTTP request)
update(id, changes) Update a row (optimistic with rollback)
upsert(row) Insert or update (optimistic with rollback)
remove(id) Delete a row (optimistic with rollback)
subscribe(filter?) Subscribe to realtime changes
hydrate() Load from local persistence
flushQueue() Flush the offline mutation queue
getQueueSize() Number of pending mutations
clearAll() Clear all records
clearAndFetch(options?) Clear cache and re-fetch (invalidation for merge mode)
mergeRecords(rows) Merge remote rows (skip pending)

Hooks

useQuery(store, options?)

Declarative data fetching with auto-refetch.

const { data, isLoading, error, refetch, isHydrated } = useQuery(
  stores.todos,
  {
    filters: [eq('completed', false)],
    sort: [{ column: 'created_at', ascending: false }],
    limit: 20,
    enabled: true,           // Toggle fetching
    deps: [statusFilter],    // Refetch when deps change
    refetchInterval: 30000,  // Auto-refetch every 30s
  },
)

useMutation(store)

Type-safe mutations with loading/error tracking.

const { insert, update, upsert, remove, isLoading, error } = useMutation(stores.todos)

await insert({ title: 'New todo', completed: false })
await update(1, { completed: true })
await remove(1)

useLinkedQuery(queryFn, options?)

Custom query that auto-refetches when linked stores mutate. Use for queries with joins or complex selects that can't use useQuery directly.

import { useLinkedQuery } from '@drakkar.software/anchor/hooks'

const { data, isLoading, error, refetch } = useLinkedQuery(
  () => fetchOfferApplications(supabase, offerId),
  {
    stores: [stores.applications],  // refetch when these stores mutate
    deps: [offerId],                // refetch when deps change
    enabled: !!offerId,
  },
)

initialData — seed data on mount to avoid loading flash (stale-while-revalidate). The network fetch still fires in the background.

const { data: offer } = useLinkedQuery(
  () => fetchOffer(supabase, id),
  {
    stores: [stores.offers],
    deps: [id],
    // Read from the store immediately — renders without a loading state
    initialData: () => stores.offers.getState().records.get(id),
  },
)

mergeToStore — write list results back into a store so detail queries can find them via initialData.

// List query populates the store as a side-effect
const { data: offers } = useLinkedQuery(
  () => fetchOffers(supabase),
  {
    stores: [stores.offers],
    mergeToStore: stores.offers,  // each fetched offer lands in the store
  },
)

// Detail query reads from the store instantly — no loading state on navigation
const { data: offer } = useLinkedQuery(
  () => fetchOffer(supabase, id),
  {
    stores: [stores.offers],
    deps: [id],
    initialData: () => stores.offers.getState().records.get(id),
  },
)

useSuspenseQuery(store, options?)

React Suspense-compatible query. Throws promise while loading.

function TodoList() {
  const data = useSuspenseQuery(stores.todos)
  return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}

// Wrap in Suspense boundary
<Suspense fallback={<Spinner />}>
  <TodoList />
</Suspense>

useInfiniteQuery(store, options)

Cursor-based infinite scroll with load-more support.

import { useInfiniteQuery } from '@drakkar.software/anchor/hooks'

function InfiniteTodoList() {
  const { data, hasNextPage, fetchNextPage, isLoading } = useInfiniteQuery(
    stores.todos,
    {
      cursorColumn: 'created_at',
      pageSize: 20,
      sort: [{ column: 'created_at', ascending: false }],
    },
  )

  return (
    <>
      <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
      {hasNextPage && <button onClick={fetchNextPage}>Load more</button>}
    </>
  )
}

useAuth(authStore)

Auth state with automatic session listener.

const { session, user, isLoading, signIn, signOut } = useAuth(stores.auth)

await signIn({ email: 'user@example.com', password: 'secret' })

useRealtime(store, options?)

Manages realtime subscription lifecycle.

const { status } = useRealtime(stores.todos)
// status: 'disconnected' | 'connecting' | 'connected' | 'error'

useRpc(supabase, functionName, args?, options?)

Call Postgres functions.

const { data, isLoading, error, refetch } = useRpc<Stats>(
  supabase, 'get_dashboard_stats', { user_id: '123' },
)

useEdgeFunction(supabase, functionName)

Invoke Supabase Edge Functions.

const { data, isLoading, invoke } = useEdgeFunction<Response>(supabase, 'send-email')
await invoke({ body: { to: 'user@example.com', subject: 'Hello' } })

useStorage(supabase, bucket)

Supabase Storage operations.

const { upload, download, getPublicUrl, list, remove, isLoading } = useStorage(supabase, 'avatars')

await upload('user-123.png', file, { upsert: true })
const url = getPublicUrl('user-123.png')

Filters

Type-safe filter DSL matching Supabase's PostgREST operators:

import {
  eq, neq, gt, gte, lt, lte,
  like, ilike, is, inValues,
  contains, containedBy, overlaps, textSearch,
  match, asc, desc,
} from '@drakkar.software/anchor'

// Comparison
eq('status', 'active')
neq('status', 'archived')
gt('priority', 3)
gte('priority', 3)
lt('priority', 10)
lte('priority', 10)

// Pattern matching
like('title', '%milk%')         // case-sensitive
ilike('title', '%milk%')        // case-insensitive

// Null/boolean check
is('deleted_at', null)

// Array/set operations
inValues('category', ['work', 'personal'])
contains('tags', ['urgent'])
containedBy('tags', ['urgent', 'important'])
overlaps('tags', ['urgent'])

// Full-text search
textSearch('body', 'hello & world', { type: 'websearch' })

// Match shorthand (multiple eq)
match({ status: 'active', priority: 1 })

// Advanced: not and filter with custom operator
// These accept { op, value } objects for the inner operator
{ column: 'status', op: 'not', value: { op: 'eq', value: 'archived' } }
{ column: 'priority', op: 'filter', value: { op: 'gt', value: 3 } }

Fluent Query Builder

Alternative to filter arrays:

import { query } from '@drakkar.software/anchor'

const result = await store.getState().fetch(
  query<Todo>()
    .where('status').eq('active')
    .where('priority').gte(3)
    .orderBy('created_at', 'desc')
    .limit(20)
    .build()
)

Cursor Pagination

Efficient keyset pagination for large datasets:

import { buildCursorQuery, processCursorResults } from '@drakkar.software/anchor'

const { filters, sort, limit } = buildCursorQuery<Todo>({
  cursorColumn: 'created_at',
  pageSize: 20,
  cursor: lastItem?.created_at,
  direction: 'forward',
})

const rows = await store.getState().fetch({ filters, sort, limit })
const { data, pagination } = processCursorResults(rows, { cursorColumn: 'created_at', pageSize: 20 })
// pagination.hasNextPage, pagination.cursor

Offline-First

Mutations are queued when offline and automatically flushed on reconnect:

const stores = createSupabaseStores<Database>({
  supabase,
  tables: ['todos'],
  persistence: { adapter: new LocalStorageAdapter() },
  network: new WebNetworkStatus(),
})

// Works offline — mutation is queued
await stores.todos.getState().insert({ title: 'Offline todo' })

// Queue status
stores.todos.getState().getQueueSize() // 1

// Manual flush
await stores.todos.getState().flushQueue()

The queue supports:

  • Coalescing — insert+update becomes single insert; insert+delete cancels both
  • Dependency trackingdependsOn field ensures parent mutations complete before children
  • Exponential backoff — retries with base * 2^attempt + jitter
  • Rollback — permanent failures restore the original state

Conflict Resolution

Five built-in strategies, configurable per table:

import { remoteWins, localWins, lastWriteWins, fieldLevelMerge } from '@drakkar.software/anchor'

createTableStore({
  // ...
  conflict: {
    strategy: 'last-write-wins',
    timestampColumn: 'updated_at',
  },
})

// Or field-level merge:
createTableStore({
  // ...
  conflict: {
    strategy: 'field-merge',
    serverOwnedFields: ['computed_score'],  // Always use server value
    clientOwnedFields: ['draft_content'],   // Always use local value
  },
})

// Or custom resolver:
createTableStore({
  // ...
  conflict: {
    strategy: 'custom',
    resolver: (local, remote, context) => ({
      ...remote,
      title: local.title,  // Keep local title, use remote for rest
    }),
  },
})

Validation

Validate data before mutations:

import { zodValidator } from '@drakkar.software/anchor'
import { z } from 'zod'

const todoSchema = z.object({
  title: z.string().min(1, 'Title required'),
  completed: z.boolean(),
})

createTableStore({
  // ...
  validate: {
    insert: zodValidator(todoSchema),
    update: zodValidator(todoSchema.partial()),
  },
})

Concurrency Safety

The library handles concurrent operations safely:

  • Concurrent fetch(): Uses a generation counter — stale responses from superseded fetches are discarded automatically
  • Concurrent mutations: Uses compare-and-swap (CAS) rollback with _anchor_mutationId — a failed update only rolls back if its own optimistic write is still current, preventing it from destroying a concurrent successful mutation's data
  • Realtime during mutations: Rows with _anchor_pending metadata are protected from being overwritten by realtime INSERT/UPDATE/DELETE events
  • Cross-tab sync: Pending optimistic rows are preserved when receiving state from other tabs
  • Offline queue: Flush uses a flushing guard to prevent concurrent execution, and in-place pruning preserves mutations enqueued during a flush

Auth Integration

Session-gated stores with automatic clear/refetch:

import { setupAuthGate, isRlsError } from '@drakkar.software/anchor'

const cleanup = setupAuthGate(supabase, stores.auth, [stores.todos, stores.profiles], {
  clearOnSignOut: true,    // Clear all stores when user signs out
  refetchOnSignIn: true,   // Refetch all stores when user signs in
})

Incremental Sync

Delta fetch — only get rows changed since last sync:

import { incrementalSync } from '@drakkar.software/anchor'

const { fetchedCount, mergedCount } = await incrementalSync(
  supabase, 'todos', 'id', stores.todos,
  { timestampColumn: 'updated_at' },
)

Cache TTL

Stale-while-revalidate pattern:

import { fetchWithSwr, setupAutoRevalidation, isStale } from '@drakkar.software/anchor'

// Serve stale data, refetch in background
await fetchWithSwr(stores.todos, { staleTTL: 5 * 60 * 1000 })

// Auto-revalidate every minute
const cleanup = setupAutoRevalidation(stores.todos, {
  staleTTL: 5 * 60 * 1000,
  checkInterval: 60 * 1000,
})

Query Cache Strategy

Control how fetch() handles existing records — replace all (default) or merge into the cache:

// Store-level: all fetches accumulate records
const todosStore = createTableStore({
  supabase,
  table: 'todos',
  cacheStrategy: 'merge', // 'replace' (default) | 'merge'
})

// In merge mode:
// - `records` accumulates all seen data (cache)
// - `order` reflects only the latest query (view)
await todosStore.getState().fetch()                                    // records: [1,2,3], order: [1,2,3]
await todosStore.getState().fetch({ filters: [eq('completed', true)] }) // records: [1,2,3], order: [2]

// Per-fetch override
await store.getState().fetch({
  filters: [eq('status', 'active')],
  cacheStrategy: 'merge', // override for this call only
})

// Invalidate accumulated cache
await store.getState().clearAndFetch()

Also available on createSupabaseStores() (global and per-table) and createViewStore().

Cross-Tab Sync

State changes sync across browser tabs:

import { setupCrossTabSync } from '@drakkar.software/anchor'

const cleanup = setupCrossTabSync(store, 'todos')
// Uses BroadcastChannel, falls back to localStorage events

Or enable via store options:

createTableStore({
  // ...
  crossTab: { enabled: true },
})

App Lifecycle

Auto-flush queue, refresh auth, and revalidate stale data when the app returns to the foreground:

import { setupAppLifecycle } from '@drakkar.software/anchor'
import { WebAppLifecycle } from '@drakkar.software/anchor-adapter-web'
// or: import { RNAppLifecycle } from '@drakkar.software/anchor-adapter-react-native'

const cleanup = setupAppLifecycle({
  adapter: new WebAppLifecycle(),
  stores: [stores.todos, stores.profiles],
  authStore: stores.auth,
  queue: offlineQueue,
  flushQueueOnForeground: true,     // default
  refreshAuthOnForeground: true,     // default
  revalidateOnForeground: true,      // default
  pauseRealtimeOnBackground: false,  // default
  staleTTL: 5 * 60 * 1000,          // 5 minutes
})

Or use the React hook:

import { useAppLifecycle } from '@drakkar.software/anchor'

useAppLifecycle({
  adapter: new WebAppLifecycle(),
  stores: [stores.todos],
  authStore: stores.auth,
})

Background Sync

Flush the offline queue in the background on mobile:

import { setupBackgroundSync } from '@drakkar.software/anchor'
import { RNBackgroundSync } from '@drakkar.software/anchor-adapter-react-native'

const cleanup = await setupBackgroundSync(offlineQueue, new RNBackgroundSync())
// Cleanup: await cleanup()

Multi-Device Sync

Sync state across devices via Supabase Realtime broadcast:

import { setupMultiDeviceSync } from '@drakkar.software/anchor'

const cleanup = setupMultiDeviceSync(supabase, {
  todos: stores.todos,
  profiles: stores.profiles,
}, {
  conflict: { strategy: 'last-write-wins', timestampColumn: 'updated_at' },
  debounceMs: 1000,
})

Selective Sync

Sync only relevant subsets of data:

import { selectiveSync, syncAllByPriority } from '@drakkar.software/anchor'

// Sync only active todos
await selectiveSync(supabase, 'todos', 'id', stores.todos, {
  filters: [eq('status', 'active')],
  timestampColumn: 'updated_at',
})

// Sync stores in priority order
await syncAllByPriority([
  { store: stores.todos, priority: 1 },
  { store: stores.profiles, priority: 2 },
])

Data Encryption

Transparently encrypt persisted data:

import { EncryptedAdapter, createWebCryptoEncryption } from '@drakkar.software/anchor'
import { LocalStorageAdapter } from '@drakkar.software/anchor-adapter-web'

const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])
const adapter = new EncryptedAdapter(new LocalStorageAdapter(), createWebCryptoEncryption(key))

createSupabaseStores({ persistence: { adapter } })

Storage Quota

Monitor and manage storage usage:

import { StorageQuotaManager } from '@drakkar.software/anchor'

const quota = new StorageQuotaManager()
const { count, estimatedBytes } = await quota.getUsage(adapter)

quota.setTableLimit('todos', 1000)
await quota.enforceLimit(adapter, 'todos')

Schema Versioning

Automatic cache invalidation on schema changes:

import { checkSchemaVersion } from '@drakkar.software/anchor'

const { versionChanged } = await checkSchemaVersion(adapter, 2)
// If version changed, all cached data is cleared and fetch() repopulates from Supabase

Sync Status Hooks

Monitor sync status across stores:

import { useSyncStatus, useQueueStatus, usePendingChanges } from '@drakkar.software/anchor'

function SyncBar() {
  const { status, pendingCount } = useSyncStatus([stores.todos, stores.profiles])
  // status: 'synced' | 'syncing' | 'offline' | 'error'

  return <div>{status} ({pendingCount} pending)</div>
}

function QueueInfo() {
  const { pendingCount, queueSize } = useQueueStatus(stores.todos)
  const pending = usePendingChanges(stores.todos)
  // pending: [{ id, row, mutationType: 'insert' | 'update' | 'delete' }]
}

Sync Metrics

Track sync health for monitoring:

import { SyncMetrics } from '@drakkar.software/anchor'

const metrics = new SyncMetrics()
createSupabaseStores({ logger: metrics })

const snap = metrics.getMetrics()
// snap.fetchLatencyP95, snap.mutationErrorCount, snap.conflictCount, ...

Conflict Audit

Log and react to conflict resolutions:

import { ConflictAuditLog } from '@drakkar.software/anchor'

const auditLog = new ConflictAuditLog()
auditLog.onConflict((entry) => {
  console.warn(`Conflict on ${entry.table}#${entry.rowId}: ${entry.strategy}`)
})

const log = auditLog.getLog({ table: 'todos', since: Date.now() - 60000 })

Retry with Backoff

Wrap any async operation with exponential backoff and jitter:

import { withRetry } from '@drakkar.software/anchor'

const result = await withRetry(() => createRpcAction(supabase, 'heavy_query')(), {
  maxRetries: 3,
  baseDelay: 1000,
})

Circuit Breaker

Protect against cascading failures from repeatedly calling failing endpoints:

import { CircuitBreaker } from '@drakkar.software/anchor'

const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 })

const result = await breaker.execute(() => fetch('/api/unstable'))
// After 5 failures: throws immediately without calling the function
// After 30s: allows one probe request (half-open state)

Rate Limiter

Throttle requests using a token bucket algorithm:

import { RateLimiter } from '@drakkar.software/anchor'

const limiter = new RateLimiter({ maxTokens: 10, refillRate: 2 }) // 10 burst, 2/sec refill

if (limiter.tryConsume()) {
  await fetch('/api/resource')
}

Aggregation

Client-side and server-side aggregation:

import { aggregateLocal, aggregateRpc } from '@drakkar.software/anchor'

// Client-side (on store data)
const stats = aggregateLocal(stores.todos, {
  total: 'count',
  avgPriority: { op: 'avg', column: 'priority' },
  maxPriority: { op: 'max', column: 'priority' },
})

// Server-side (via Postgres function)
const serverStats = await aggregateRpc(supabase, 'aggregate_todos', { user_id: '123' })

Storage

Full Supabase Storage support:

import { createStorageActions } from '@drakkar.software/anchor'

const avatars = createStorageActions(supabase, 'avatars')

await avatars.upload('user-123.png', file, { upsert: true })
const url = avatars.getPublicUrl('user-123.png')
const { signedUrl } = await avatars.createSignedUrl('private/doc.pdf', { expiresIn: 3600 })
const files = await avatars.list('uploads/')
await avatars.remove(['old-file.png'])

Edge Functions

import { createEdgeFunctionAction } from '@drakkar.software/anchor'

const sendEmail = createEdgeFunctionAction<{ success: boolean }>(supabase, 'send-email')
const result = await sendEmail({ body: { to: 'user@example.com', subject: 'Hello' } })

RPC (Postgres Functions)

import { createRpcAction } from '@drakkar.software/anchor'

const getStats = createRpcAction<DashboardStats>(supabase, 'get_dashboard_stats')
const { data, error } = await getStats({ user_id: '123' })

Server Components (RSC)

Server-side prefetch for React Server Components:

// app/todos/page.tsx (Server Component)
import { prefetch } from '@drakkar.software/anchor'

export default async function TodosPage() {
  const { data } = await prefetch<Todo>(supabase, 'todos', {
    sort: [{ column: 'created_at', ascending: false }],
    limit: 50,
  })

  return <TodoList initialData={data} />
}

Platform Adapters

Web

import {
  LocalStorageAdapter, IndexedDBAdapter,
  WebNetworkStatus, WebAppLifecycle,
} from '@drakkar.software/anchor-adapter-web'

new LocalStorageAdapter()   // Small datasets (<5MB)
new IndexedDBAdapter()      // Large datasets
new WebNetworkStatus()      // Network detection
new WebAppLifecycle()       // App lifecycle (Page Visibility API)

React Native

import {
  ExpoSqliteAdapter, AsyncStorageAdapter,
  RNNetworkStatus, RNAppLifecycle,
  RNBackgroundSync, createExpoOAuthHandler,
} from '@drakkar.software/anchor-adapter-react-native'

new ExpoSqliteAdapter()     // Structured (recommended)
new AsyncStorageAdapter()   // Simple fallback
new RNNetworkStatus()       // Network detection
new RNAppLifecycle()        // App lifecycle (AppState API)
new RNBackgroundSync()      // Background task (expo-task-manager)
createExpoOAuthHandler(supabase)  // OAuth with deep links

Middleware

Devtools

createTableStore({
  // ...
  devtools: true,
  // or with custom name:
  devtools: { name: 'todos-store' },
})

Immer

import { immer } from 'zustand/middleware/immer'

createTableStore({
  // ...
  immer, // Pass the middleware function
})

Packages

Package Description
@drakkar.software/anchor Core library
@drakkar.software/anchor-adapter-web Web: localStorage, IndexedDB, WebNetworkStatus, WebAppLifecycle
@drakkar.software/anchor-adapter-react-native React Native: expo-sqlite, AsyncStorage, NetInfo, AppLifecycle, BackgroundSync, OAuth

Tree-Shakeable Imports

// Full API
import { createTableStore, useQuery, eq } from '@drakkar.software/anchor'

// Hooks only
import { useQuery, useMutation, useSyncStatus } from '@drakkar.software/anchor/hooks'

// Query builder only
import { query, QueryBuilder } from '@drakkar.software/anchor/query/queryBuilder'

// Server-only (no React dependency)
import { prefetch } from '@drakkar.software/anchor/server/prefetch'

// Storage only
import { createStorageActions } from '@drakkar.software/anchor/storage/storageActions'

// New entry points
import { setupAppLifecycle } from '@drakkar.software/anchor/lifecycle'
import { setupBackgroundSync } from '@drakkar.software/anchor/sync/background'
import { selectiveSync } from '@drakkar.software/anchor/sync/selective'
import { setupMultiDeviceSync } from '@drakkar.software/anchor/sync/multiDevice'
import { SyncMetrics } from '@drakkar.software/anchor/sync/metrics'
import { EncryptedAdapter } from '@drakkar.software/anchor/persistence/encrypted'
import { StorageQuotaManager } from '@drakkar.software/anchor/persistence/quota'
import { checkSchemaVersion } from '@drakkar.software/anchor/persistence/schemaVersion'
import { ConflictAuditLog } from '@drakkar.software/anchor/mutation/audit'

Requirements

  • zustand >= 4.5.0
  • @supabase/supabase-js >= 2.0.0
  • TypeScript >= 5.0 (recommended)
  • React >= 18.0 (optional, for hooks)
  • immer (optional, for draft-based mutations)

License

MIT

About

Type-safe Zustand stores auto-generated from your Supabase schema. Offline-first, realtime, with optimistic updates.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors