Offline-first sync engine for Supabase in React Native using SQLite
npm install supastash
Offline-First Sync Engine for Supabase + React Native
> Supastash syncs your Supabase data with SQLite โ live, offline, and conflict-safe. No boilerplate. Built for React Native and Expo.
---
โ Full Docs
โ Getting Started Guide
---
- ๐ Two-way sync (Supabase โ SQLite)
- ๐พ Offline-first querying with local cache
- โก Realtime updates (INSERT, UPDATE, DELETE)
- ๐ Compatible with all major SQLite clients:
- expo-sqlite
- react-native-nitro-sqlite
- react-native-sqlite-storage (beta)
- ๐ง Built-in:
- Conflict resolution
- Sync retries
- Batched updates
- Row-level filtering
- Staged job processing
---
``bash`
npm install supastash
`bash`
npm install @supabase/supabase-js \
@react-native-community/netinfo \
react \
react-native
Choose only one, based on your stack:
`bashExpo (most common)
npm install expo-sqlite
> Match with
sqliteClientType: "expo", "rn-nitro", or "rn-storage"---
โ๏ธ Quick Setup
$3
`ts
// lib/supastash.ts
import { configureSupastash, defineLocalSchema } from "supastash";
import { supabase } from "./supabase";
import { openDatabaseAsync } from "expo-sqlite"; // or your adapterconfigureSupastash({
supabaseClient: supabase,
dbName: "supastash_db",
sqliteClient: { openDatabaseAsync },
sqliteClientType: "expo",
onSchemaInit: () => {
defineLocalSchema("users", {
id: "TEXT PRIMARY KEY",
name: "TEXT",
email: "TEXT",
created_at: "TIMESTAMP",
updated_at: "TIMESTAMP",
});
},
debugMode: true,
syncEngine: {
push: true,
pull: false, // enable this if using filters or RLS
},
excludeTables: {
push: ["daily_reminders"],
pull: ["daily_reminders"],
},
});
`$3
`ts
// App.tsx or _layout.tsx
import "@/lib/supastash"; // triggers init
import { useSupatash } from "supastash";export default function App() {
const { dbReady } = useSupatash();
if (!dbReady) return null;
return ;
}
`---
$3
To auto-update Zustand stores when local data changes, listen for refresh events:
`ts
supastashEventBus.on("supastash:refreshZustand:orders", hydrateOrders);
`Use this in a hook like
useHydrateStores() to stay in sync without polling.
๐ Read Docs---
$3
`ts
const { data, groupedBy } = useSupastashData("orders", {
filter: { column: "user_id", operator: "eq", value: userId },
extraMapKeys: ["status"],
realtime: true, // Default: true
});
`- โ
Auto-syncs with Supabase Realtime
- โ
Keeps your UI in sync automatically
- โ
Ideal for dashboards, chat, shared data
---
$3
If you use
pull: true, you must define filters per table:`ts
useSupastashFilters({
orders: [{ column: "shop_id", operator: "eq", value: activeShopId }],
inventory: [{ column: "location_id", operator: "eq", value: location }],
});
`> Without filters or RLS, Supastash may try to pull full tables โ which could lead to empty results or large sync payloads.
---
๐จ Important Notes
- Your Supabase tables must have:
- A primary key
id (string or UUID)
- timestamptz columns for created_at, updated_at, and deleted_at- Run this SQL in Supabase to allow schema reflection:
`sql
create or replace function get_table_schema(table_name text)
returns table(column_name text, data_type text, is_nullable text)
security definer
as $$
select column_name, data_type, is_nullable
from information_schema.columns
where table_schema = 'public' and table_name = $1;
$$ language sql;grant execute on function get_table_schema(text) to anon, authenticated;
`---
๐งช Example:
useSupatashData`tsx
import { useSupatashData } from "supastash";const { data: orders } = useSupatashData("orders", {
filter: { column: "user_id", operator: "eq", value: userId },
});
`---
๐ How Sync Works
- Tracks rows using
updated_at, deleted_at, and created_at
- Batches changes in background and retries failed ones
- Local cache backed by Supabase
- Runs pull/push jobs efficiently using staged task pipelines---
๐ง Advanced Querying (Optional)
Supastash includes a built-in query builder:
`ts
await supastash
.from("orders")
.update({ status: "delivered" })
.syncMode("localFirst") // localOnly, remoteOnly also available
.run();
`---
๐ง API Docs
configureSupastash()
- useSupatashData()
- useSupastashFilters()
- supastash.from(...).run()`---
PRs are welcome! Please write clear commit messages and add tests when relevant.
---
MIT ยฉ Ezekiel Akpan
---
Open an issue or reach out on Twitter/X
---