supastash
Version:
Offline-first sync engine for Supabase in React Native using SQLite
273 lines (189 loc) โข 5.99 kB
Markdown
# Supastash
[](https://www.npmjs.com/package/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](https://0xzekea.github.io/supastash/)
## โจ Features
- ๐ **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`
- ๐ง Built-in:
- Conflict resolution
- Sync retries
- Batched updates
- Row-level filtering
- Staged job processing
## ๐ฆ Installation
```bash
npm install supastash
```
### โ Required Peer Dependencies
```bash
npm install @supabase/supabase-js \
@react-native-community/netinfo \
react \
react-native
```
### ๐งฑ Choose a SQLite Adapter
Choose **only one**, based on your stack:
```bash
# Expo (most common)
npm install expo-sqlite
# Bare RN with better speed
npm install react-native-nitro-sqlite
# Classic RN SQLite
npm install react-native-sqlite-storage
```
> Match with `sqliteClientType`: `"expo"`, `"rn-nitro"`, or `"rn-storage"`
## โ๏ธ Quick Setup
### 1. Configure Supastash
```ts
// 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
```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 <Stack />;
}
```
### ๐ง Optional: Zustand Auto-Hydration
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](https://0xzekea.github.io/supastash/docs/zustand)**
### ๐ `useSupastashData` (Global, Realtime)
```ts
const { data, groupedBy } = useSupastashData("orders", {
filter: { column: "user_id", operator: "eq", value: userId },
extraMapKeys: ["status"],
});
```
- โ
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:
```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 optionally `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
- Keeps **local database as the source of truth**
- 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();
```
## ๐ Recommended Project Structure
```
src/
โโ core/ # Supastash config, Supabase client
โโ hooks/ # useSupatashData, useSupastashFilters etc.
โโ types/ # Zod schemas, DB types
โโ utils/ # Local helpers
```
## ๐งช Dev & Testing
```bash
yarn test # Run tests
yarn dev # Dev mode (watch)
```
## ๐ง API Docs
- [`configureSupastash()`](https://0xzekea.github.io/supastash/docs/configuration)
- [`useSupatashData()`](https://0xzekea.github.io/supastash/docs/useSupastashData)
- [`useSupastashFilters()`](https://0xzekea.github.io/supastash/docs/useSupastashFilters)
- [`supastash.from(...).run()`](https://0xzekea.github.io/supastash/docs/supastash-query-builder)
## ๐ค Contributing
PRs are welcome! Please write clear commit messages and add tests when relevant.
## ๐ License
MIT ยฉ [Ezekiel Akpan](https://x.com/0xZekeA)
## ๐ฌ Need Help?
Open an issue or reach out on [Twitter/X](https://x.com/0xZekeA)