Data Layer
Use typed Supabase clients, TanStack Query, and persistence patterns.
The Expo template's data layer is the React Native equivalent of the repository pattern used in the native templates: typed Supabase calls live in small feature modules, while TanStack Query owns loading, caching, retries, pagination, offline awareness, and mutation refresh.
Stack
| Layer | File | Role |
|---|---|---|
| Supabase client | lib/supabase.ts | createClient<Database>() typed by types/database.ts |
| Query provider | lib/query.tsx | One global QueryClient, offline-first defaults, focus/online managers |
| Query keys | lib/query.tsx | Centralized stable keys for profile, todos, uploads, subscription, version config |
| Feature modules | lib/profile.ts, lib/sample-uploads.ts, lib/referrals.ts | Keep query and mutation functions out of screen components |
| Screen queries | app/todos.tsx, app/upload-sample.tsx | Compose feature calls into UI state, optimistic cache updates, and toasts |
Run type generation after schema changes:
pnpm gen:typesThen import table types from types/database.ts instead of hand-writing row
shapes when possible.
Query provider
QueryProvider wraps the app in PersistQueryClientProvider with these defaults:
| Setting | Value | Effect |
|---|---|---|
networkMode | offlineFirst | Queries and mutations tolerate temporary offline state |
| Query retry | Up to 2 transient failures | Network-like errors retry; validation/auth errors do not |
staleTime | 1 minute | Recent successful data avoids immediate refetch churn |
gcTime | 24 hours | In-memory cache survives normal app navigation |
| Persistence | AsyncStorage key SOAR_QUERY_CACHE | Successful queries can restore across launches for up to 7 days |
NetInfo drives TanStack Query's online manager. On native, AppState drives the focus manager so foregrounding the app can refresh stale data.
Feature modules
Keep reusable data calls in lib/ and let screens own presentation state.
lib/profile.tsfetches and upsertsprofiles, compresses avatar images withexpo-image-manipulator, uploads to anavatarsbucket, and resolves signed URLs.lib/sample-uploads.tsreads recentsample_uploadsrows and uploads files through Supabase Storage with XMLHttpRequest progress.lib/referrals.tsbuilds a share link from the deep-link helper.lib/network.tsmaps transient failures into friendly user-facing messages and exposesretryAsync.lib/cache.tsstores small JSON payloads in AsyncStorage or localStorage.
profile.ts references an avatars bucket, but the shipped migrations create
only sample-uploads. If you keep avatar uploads, add an avatars bucket and
matching policies for your project.
Todos reference feature
app/todos.tsx is the most complete CRUD reference:
useInfiniteQuerypagestodos20 rows at a time.- Queries filter by
user_id, while RLS still enforces ownership server-side. - Add, toggle, edit, delete, and clear-completed mutations update the query
cache with
queryClient.setQueryData(...)on success. - Reorder writes
sort_orderupdates to Supabase. - If a refetch fails while cached rows exist, the UI shows the last saved todos and a reconnect toast.
This is optimistic in the user-facing sense that successful mutation results are merged into cache immediately; it does not pretend failed writes succeeded.
Offline behavior
The app has three layers of offline behavior:
- TanStack Query persists successful query results across launches.
- NetInfo marks the QueryClient online/offline and can drive an
OfflineBanner. toNetworkFriendlyMessageconverts transient errors into consistent copy.
components/offline-banner.tsx is implemented, but the root layout does not
currently mount NetworkStatusProvider and OfflineBanner globally. Add them
around the navigator if your product needs a persistent app-wide offline banner.
RLS expectations
Client code filters by the signed-in user, but trust comes from Postgres RLS:
| Table | Client access |
|---|---|
profiles | Users select, insert, and update their own profile row |
todos | Users select, insert, update, and delete their own todos |
sample_uploads | Users select, insert, and delete their own upload rows |
storage.objects for sample-uploads | Users can read, insert, update, and delete objects whose first folder segment is their user ID |
subscriptions / payments | Users can select their own rows only; service-role Edge Functions write them |
Add a feature
Add a migration
Create the table, indexes, triggers, and owner-scoped RLS policy in
supabase/migrations/.
Create a lib/ module
Keep Supabase from(...), storage.from(...), and data mapping in a small
module instead of scattering it through screens.
Add query keys and hooks
Add stable keys in queryKeys, then call your module from useQuery,
useInfiniteQuery, or useMutation.
Render the screen
Handle signed-out state with SignInGate, map errors with
toNetworkFriendlyMessage, and update/invalidate cached data after mutations.
