State and data
Status: Scaffolded 2026-09-24
Two kinds of state, two tools, and a deliberately short list of what survives an app restart. The rule that decides most questions here: the server is the source of truth for anything about a member, and the device holds only what it needs to ask the server.
Auth: zustand
src/lib/stores/authStore.ts is the only zustand store. It holds:
{
status: 'restoring' | 'signedOut' | 'signedIn',
user: User | null, // the GET /api/me shape
token: string | null, // in memory, mirrored to SecureStore
restore(), signIn({ token, user }), signOut(), refreshUser()
}
signIn writes the token to SecureStore and sets signedIn. signOut calls POST /api/auth/logout (best effort), deletes the SecureStore entry, clears the TanStack Query cache, and sets signedOut. restore is called once by AuthGate (navigation). No other UI state lives in zustand; a screen's local state is useState.
Server data: TanStack Query
Every read from the API goes through a query with a key per resource, so invalidation after a mutation is a one-liner and screens never hold their own copies.
| Key | Fetches | Notes |
|---|---|---|
['me'] | GET /api/me | staleTime long; refreshed after PATCH |
['places', bbox, { type, q }] | GET /api/places?bbox= | keyed on the rounded viewport so panning a few meters reuses the cache |
['place', slug] | GET /api/places/:slug | |
['events', { from, to, area, groveId }] | infinite query over nextCursor | |
['event', slug] | ||
['feed', scope] | infinite query | scope is friends, grove:<id>, or public |
['conversations'], ['messages', conversationId] | messages also arrive over Socket.IO and are merged into the cache | |
['friends'], ['friendRequests'] | ||
['media', filters], ['guides', category] | public, long staleTime |
Mutations (useMutation) invalidate the keys they affect and nothing broader. Lists use useInfiniteQuery with getNextPageParam: (last) => last.nextCursor ?? undefined, matching the API's cursor contract. The query client is created once in app/_layout.tsx with a default staleTime of a minute and retry: 1; a 401 inside a query calls authStore.signOut() from the client's onError.
The query cache is memory only. There is no persistQueryClient; on restart the app refetches, which is a few small requests.
Persisted on the device
| What | Where | Why |
|---|---|---|
| Session token | expo-secure-store | the Keychain or Keystore, encrypted by the OS, unavailable to other apps and to backups |
Theme preference (system, dark, light) | AsyncStorage | a UI preference with no privacy weight; added with the theme toggle, not in the scaffold's dependencies yet |
That is the whole list. Not persisted: the member object (refetched from ['me']), any query cache, drafts, the last map position, location, or friends. A stolen or shared device yields one revocable token and a theme.
Location
expo-location is requested when-in-use the first time the places tab mounts, to center the map. The coordinate is passed to the map component and dropped. It is never stored, never put in a query key as itself (the key holds the bounding box the viewport settled on, which is what the API receives), and never sent anywhere else. Denying the permission leaves the map on its default Southern California view.