Architecture
An overview of how the starter is wired together: the provider tree, navigation, state, Relay data fetching, and the file layout.
Provider tree
src/App.tsx is the root. The app is wrapped with Sentry.wrap(...) and the providers are nested (outermost first):
<GlobalStoreProvider> // easy-peasy store + persistence
<FeatureFlagProvider> // Unleash (no-op until configured)
<RelayEnvironmentProvider>
<SafeAreaProvider>
<ScreenDimensionsProvider>
<ThemeProvider> // @artsy/palette-mobile theme
<GestureHandlerRootView>
<GlobalRetryErrorBoundary>
<SuspenseWrapper>
<Main /> // navigation entry pointsetupSentry() runs once at module load, before the component tree renders.
Navigation
Navigation uses react-navigation v7's static API, defined in src/Navigation.tsx.
- A native-stack
RootStacksplits into two conditional groups guarded by auth state read from the store:SignedIn(useIsLoggedIn) → theHomeTabsbottom-tab navigator (Home,List,Settings).SignedOut(useIsLoggedOut) → theLoginscreen.
- The param list is registered on the global
ReactNavigation.RootParamListnamespace viaStaticParamList<typeof RootStack>, souseNavigation()is typed everywhere without manual param types. createStaticNavigation(RootStack)produces the navigator. The exportedMaincomponent waits for the persisted store to rehydrate (useStoreRehydrated) before rendering, so the auth guards resolve to the correct group on first paint.
Screens live under src/Scenes/ (one folder per top-level screen).
State management
Global state uses easy-peasy, configured in src/store/GlobalStore.tsx:
- The store is created with
createStore(persist(GlobalStoreModel, { storage }))and persisted toAsyncStorage. - Typed hooks come from
createTypedHooks<GlobalStoreModel>(). Read state withGlobalStore.useAppState(...); dispatch throughGlobalStore.actions....
// Read
const token = GlobalStore.useAppState((state) => state.auth.userAccessToken)
// Dispatch
GlobalStore.actions.auth.signOut()Models live in src/store/Models/ (AuthModel, ConfigModel, EnvironmentModel, and the root GlobalStoreModel).
Store versioning
The store is persisted with a STORE_VERSION. If you change a Model's shape, bump the version and add a migration so persisted state upgrades cleanly.
Relay & GraphQL
Data fetching uses Relay 20 against Artsy's Metaphysics GraphQL API.
- Load queries through
useSystemQueryLoader(fromsrc/system/relay/) rather than callinguseLazyLoadQuerydirectly, and access the Relay environment throughuseSystemRelayEnvironmentrather thanuseRelayEnvironment. Today both are thin drop-in wrappers, but they are the single seam where cross-cutting behavior (offline guards, global fetch policy/key, environment resets) can be added later without touching call sites — mirroring Eigen/Energy. - Prefer Relay hooks (
useFragment,usePaginationFragment) over HOCs, and co-locate fragments with the component that consumes them. - Run
yarn relayafter changing any query or fragment. Generated artifacts live insrc/__generated__/and are never edited by hand (they're gitignored except for.gitkeep). - The Relay environment is set up in
src/relay/(network middlewares undersrc/relay/middlewares/) and provided viaRelayEnvironmentProviderinApp.tsx. yarn type-checkruns the Relay compiler beforetsc.
Logging
App code logs through the structured, leveled logger in src/system/logger/ — never console.* directly:
import { logger } from "system/logger"
logger.debug("cache hit", { key })
logger.info("user signed in", { userId })
logger.warn("slow request", { ms })
logger.error("failed to open app", err, { url })The logger is the single logging seam: in development it prints level-prefixed console output, and in production it is wired to Sentry (debug/info become breadcrumbs, warn captures a warning, error captures the passed Error as an exception). The minimum level defaults to debug in dev and info in production. Routing every call site through one module means future transports (file, remote, etc.) can be added in one place.
File organization
src/
├── App.tsx # Root component + provider tree
├── Navigation.tsx # Route definitions + navigation stack
├── Scenes/ # Top-level screens (Home, List, Login, Settings)
├── components/ # Shared UI components across scenes
├── helpers/ # Shared utilities
├── relay/ # Relay environment + network middlewares
├── store/ # easy-peasy global store + Models
├── system/ # Core infra
│ ├── devTools/ # Sentry setup
│ ├── logger/ # Structured leveled logger (Sentry-wired)
│ ├── providers/ # Theme, FeatureFlag providers
│ ├── relay/ # useSystemQueryLoader / useSystemRelayEnvironment
│ └── wrappers/ # Suspense + error-boundary wrappers
├── assets/ # Images and fonts
├── utils/test/ # Test helpers (setupTestWrapper, renderWithWrappers)
└── __generated__/ # Relay-generated artifacts (do not edit)
data/
└── schema.graphql # Metaphysics GraphQL schema
e2e/
└── flows/ # Recorded agent-device .ad e2e checksConventions
- Use absolute imports from
src/(module-resolver root is./src). Only same-folder relative imports are allowed. - Do not import components/hooks directly from another Scene — extract shared code to
src/components/(UI) orsrc/helpers/(utilities). - Use
@artsy/palette-mobileprimitives (Flex,Text,Button,Input,Spacer,useColor,useSpace,Theme) rather than re-implementing them. - Use Luxon for date/time — do not add moment.
- Log through the
logger(system/logger) instead ofconsole.*. - Load Relay queries via
useSystemQueryLoaderand read the environment viauseSystemRelayEnvironment(not the raw Relay hooks). - Imports are auto-sorted by
eslint-plugin-simple-import-sort(runyarn lint), andeslint-plugin-react-native-a11yflags missing accessibility props on interactive components (atwarn). - Naming: components
PascalCase.tsx, hooksuseCamelCase, constantsUPPER_SNAKE_CASE, utilitiescamelCase.ts. - PR titles follow Conventional Commits (
feat,fix,docs,chore,refactor,test,ci,build,perf) — enforced by CI.