supastash
v0.1.48
Published
Offline-first sync engine for Supabase in React Native using SQLite
Maintainers
Readme
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.
📚 Documentation
→ Full Docs
→ Getting Started Guide
✨ Features
🔁 Two-way sync (Supabase ↔ SQLite)
💾 Offline-first querying with local cache
⚡ Realtime updates (INSERT, UPDATE, DELETE)
🔌 Compatible with all major SQLite clients:
expo-sqlitereact-native-nitro-sqlitereact-native-sqlite-storage(beta)
🧠 Built-in:
- Conflict resolution
- Sync retries
- Batched updates
- Row-level filtering
- Staged job processing
📦 Installation
npm install supastash➕ Required Peer Dependencies
npm install @supabase/supabase-js \
@react-native-community/netinfo \
react \
react-native🧱 Choose a SQLite Adapter
Choose only one, based on your stack:
# Expo (most common)
npm install expo-sqlite
# Bare RN with better speed
npm install react-native-nitro-sqlite
# Classic RN SQLite (beta)
npm install react-native-sqlite-storageMatch with
sqliteClientType:"expo","rn-nitro", or"rn-storage"
⚙️ Quick Setup
1. Configure Supastash
// lib/supastash.ts
import { configureSupastash, defineLocalSchema } from "supastash";
import { supabase } from "./supabase";
import { openDatabaseAsync } from "expo-sqlite"; // or your adapter
configureSupastash({
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"],
},
});2. Initialize Once
// 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 <Stack />;
}🧠 Optional: Zustand Auto-Hydration
To auto-update Zustand stores when local data changes, listen for refresh events:
supastashEventBus.on("supastash:refreshZustand:orders", hydrateOrders);Use this in a hook like useHydrateStores() to stay in sync without polling.
👉 Read Docs
🔁 useSupastashData (with Realtime)
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
🛡️ Use Filters for Pull Syncing
If you use pull: true, you must define filters per table:
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) timestamptzcolumns forcreated_at,updated_at, anddeleted_at
- A primary key
Run this SQL in Supabase to allow schema reflection:
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
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, andcreated_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:
await supastash
.from("orders")
.update({ status: "delivered" })
.syncMode("localFirst") // localOnly, remoteOnly also available
.run();🔧 API Docs
🤝 Contributing
PRs are welcome! Please write clear commit messages and add tests when relevant.
📜 License
MIT © Ezekiel Akpan
💬 Need Help?
Open an issue or reach out on Twitter/X
