Type-safe Zustand stores auto-generated from your Supabase schema. Offline-first, realtime, with optimistic updates.
- Auto-generated, type-safe stores from Supabase
Databasetypes — 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)
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-nativenpx supabase gen types typescript --project-id $PROJECT_REF > database.types.tsimport { 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',
})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>
)
}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, andofflineQueueoptions requirecreateSupabaseStores()which wires up the shared RealtimeManager and OfflineQueue. UsecreateSupabaseStores()for full-featured stores, or manually set up these features withRealtimeManager,bindRealtimeToStore, andOfflineQueue.
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 subscriptionsCreates 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"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) |
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
},
)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)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),
},
)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>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>}
</>
)
}Auth state with automatic session listener.
const { session, user, isLoading, signIn, signOut } = useAuth(stores.auth)
await signIn({ email: 'user@example.com', password: 'secret' })Manages realtime subscription lifecycle.
const { status } = useRealtime(stores.todos)
// status: 'disconnected' | 'connecting' | 'connected' | 'error'Call Postgres functions.
const { data, isLoading, error, refetch } = useRpc<Stats>(
supabase, 'get_dashboard_stats', { user_id: '123' },
)Invoke Supabase Edge Functions.
const { data, isLoading, invoke } = useEdgeFunction<Response>(supabase, 'send-email')
await invoke({ body: { to: 'user@example.com', subject: 'Hello' } })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')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 } }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()
)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.cursorMutations 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 tracking —
dependsOnfield ensures parent mutations complete before children - Exponential backoff — retries with
base * 2^attempt + jitter - Rollback — permanent failures restore the original state
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
}),
},
})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()),
},
})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_pendingmetadata 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
flushingguard to prevent concurrent execution, and in-place pruning preserves mutations enqueued during a flush
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
})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' },
)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,
})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().
State changes sync across browser tabs:
import { setupCrossTabSync } from '@drakkar.software/anchor'
const cleanup = setupCrossTabSync(store, 'todos')
// Uses BroadcastChannel, falls back to localStorage eventsOr enable via store options:
createTableStore({
// ...
crossTab: { enabled: true },
})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,
})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()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,
})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 },
])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 } })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')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 SupabaseMonitor 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' }]
}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, ...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 })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,
})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)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')
}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' })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'])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' } })import { createRpcAction } from '@drakkar.software/anchor'
const getStats = createRpcAction<DashboardStats>(supabase, 'get_dashboard_stats')
const { data, error } = await getStats({ user_id: '123' })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} />
}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)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 linkscreateTableStore({
// ...
devtools: true,
// or with custom name:
devtools: { name: 'todos-store' },
})import { immer } from 'zustand/middleware/immer'
createTableStore({
// ...
immer, // Pass the middleware function
})| 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 |
// 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'- 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)
MIT
