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

LayerFileRole
Supabase clientlib/supabase.tscreateClient<Database>() typed by types/database.ts
Query providerlib/query.tsxOne global QueryClient, offline-first defaults, focus/online managers
Query keyslib/query.tsxCentralized stable keys for profile, todos, uploads, subscription, version config
Feature moduleslib/profile.ts, lib/sample-uploads.ts, lib/referrals.tsKeep query and mutation functions out of screen components
Screen queriesapp/todos.tsx, app/upload-sample.tsxCompose feature calls into UI state, optimistic cache updates, and toasts

Run type generation after schema changes:

pnpm gen:types

Then 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:

SettingValueEffect
networkModeofflineFirstQueries and mutations tolerate temporary offline state
Query retryUp to 2 transient failuresNetwork-like errors retry; validation/auth errors do not
staleTime1 minuteRecent successful data avoids immediate refetch churn
gcTime24 hoursIn-memory cache survives normal app navigation
PersistenceAsyncStorage key SOAR_QUERY_CACHESuccessful 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.ts fetches and upserts profiles, compresses avatar images with expo-image-manipulator, uploads to an avatars bucket, and resolves signed URLs.
  • lib/sample-uploads.ts reads recent sample_uploads rows and uploads files through Supabase Storage with XMLHttpRequest progress.
  • lib/referrals.ts builds a share link from the deep-link helper.
  • lib/network.ts maps transient failures into friendly user-facing messages and exposes retryAsync.
  • lib/cache.ts stores 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:

  • useInfiniteQuery pages todos 20 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_order updates 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.
  • toNetworkFriendlyMessage converts 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:

TableClient access
profilesUsers select, insert, and update their own profile row
todosUsers select, insert, update, and delete their own todos
sample_uploadsUsers select, insert, and delete their own upload rows
storage.objects for sample-uploadsUsers can read, insert, update, and delete objects whose first folder segment is their user ID
subscriptions / paymentsUsers 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/.

Regenerate types

pnpm gen:types

Use the generated Tables<"your_table"> helper where it fits.

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.

On this page