A practical starting point for iOS and Android, built with Expo and TypeScript.
Feature-based architecture, validated API data, light and dark themes, and automated checks.
Get started · Explore the structure · Customize your app · Contribute · Get help
This template brings Expo Router, TanStack Query, zustand, and zod together with strict TypeScript, Jest, ESLint, Prettier, and GitHub Actions. Shared agent instructions and Claude Code configuration are included for teams using coding assistants.
The example Posts feature includes a list and detail screen backed by JSONPlaceholder.
Settings persists a light, dark, or system theme. Both provide working patterns to adapt
for your product; the template app identity remains TemplateApp until you rename it.
- Stack
- Prerequisites
- Setup
- Scripts
- Project structure
- Native folders
- Configuration and environments
- Builds and releases
- CI
- Claude Code
- Rename checklist
- Decision points
- Known limitations
- Contributing and support
- License
| Concern | Choice |
|---|---|
| Framework | Expo SDK 58 (React Native 0.88, React 19.3) |
| Language | TypeScript 6.0, strict + noUncheckedIndexedAccess |
| Navigation | Expo Router (file-based, typed routes) |
| Server state / API | TanStack Query 5 + fetch wrapper (src/lib/api/client.ts) + zod 4 validation |
| Client state | zustand 5, persisted with AsyncStorage |
| Theming | Design tokens in src/theme/tokens.ts, light and dark palettes |
| Testing | Jest 29 (jest-expo preset) + @testing-library/react-native 14 |
| Lint / format | ESLint 9 (eslint-config-expo flat config) + Prettier 3, eslint-config-prettier |
| Builds | Continuous Native Generation (expo prebuild) locally; EAS Build for releases |
| CI | GitHub Actions: typecheck, lint, format, tests, expo-doctor, prebuild |
Why Expo instead of the bare React Native CLI. The React Native docs recommend starting new
apps with a framework and name Expo
(reactnative.dev/docs/environment-setup). Expo
provides file-based routing, SDK-matched dependency versions (npx expo install), config plugins
for native setup, and cloud builds. Native code stays fully available: npm run prebuild
generates standard Xcode and Gradle projects, and any React Native library with native code works
in a development build. The trade-off is that native changes go through app.config.ts and config
plugins rather than direct edits (see Native folders).
The dependency versions are declared in package.json and locked in
package-lock.json. Use npx expo install for SDK-compatible additions and npm run doctor
to check your local dependency and configuration health.
- Node.js 22.13 or newer in the 22.x line (
.nvmrc;nvm use). Expo SDK 58 supports^22.13.0 || ^24.3.0 || ^26.0.0. - npm 10 (ships with Node 22). The lockfile is
package-lock.json. - iOS: macOS with Xcode and an iOS Simulator, or an EAS account for cloud builds.
- Android: Android Studio with an SDK and an emulator, or an EAS account for cloud builds.
- Full environment guide: docs.expo.dev/get-started/set-up-your-environment.
-
Create your repository from this template (GitHub Use this template), or clone it.
-
Install dependencies and create your env file:
nvm use npm ci cp .env.example .env
-
Rename the template: see Rename checklist. You can do this later, but bundle IDs are hard to change after a store release.
-
Run on a simulator or device. This builds a development build (includes
expo-dev-client):npm run ios # macOS + Xcode npm run android # Android SDK + emulator or device
After the first build, start only the JS bundler with
npm start.Without local native tooling, build the development client in the cloud instead:
npx eas-cli@latest build --profile development --platform ios|android. -
Before committing:
npm run check.
The app fails at startup with a clear message if EXPO_PUBLIC_API_BASE_URL is missing or not a
valid http(s) URL.
| Script | What it does |
|---|---|
npm start |
Start Metro for a development build |
npm run ios |
Generate ios/ if needed, compile, install and launch on simulator |
npm run android |
Generate android/ if needed, compile, install and launch |
npm run prebuild |
Regenerate ios/ and android/ from app.config.ts (--clean) |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint, fails on any warning |
npm run lint:fix |
ESLint with autofix |
npm run format |
Prettier write |
npm run format:check |
Prettier check |
npm test |
Jest. One file: npm test -- src/features/posts |
npm run check |
typecheck + lint + format:check + test. Same as the CI checks job |
npm run doctor |
expo-doctor: dependency versions and config problems |
npm run rename |
Rename the app identity (see Rename checklist) |
Add dependencies with npx expo install <package>, not npm install <package>, so versions match
the SDK.
.
├── AGENTS.md # Agent instructions shared by all coding tools
├── CLAUDE.md # Claude Code memory: imports AGENTS.md + Claude-specific notes
├── .claude/ # Claude Code configuration (see "Claude Code")
├── .github/
│ ├── README.md # Repository overview and setup (this file)
│ ├── CONTRIBUTING.md # Development workflow and review expectations
│ ├── SUPPORT.md # Troubleshooting and where to ask for help
│ ├── ISSUE_TEMPLATE/ # Bug, improvement, and documentation forms
│ ├── pull_request_template.md # Change summary and verification
│ ├── dependabot.yml # GitHub Actions version updates
│ └── workflows/ci.yml # Quality checks and native configuration validation
├── app.config.ts # App identity, icons, splash, plugins, build variants
├── eas.json # EAS Build profiles: development, preview, production
├── assets/images/ # Placeholder icon and splash artwork (+ README with sizes)
├── scripts/rename.mjs # Rename script
├── src/
│ ├── app/ # Expo Router routes only
│ │ ├── _layout.tsx # Root: providers, theme, splash, Stack
│ │ ├── +not-found.tsx
│ │ ├── (tabs)/
│ │ │ ├── _layout.tsx # Tab navigator
│ │ │ ├── index.tsx # Posts tab
│ │ │ └── settings.tsx # Settings tab
│ │ └── posts/[id].tsx # Post detail
│ ├── features/
│ │ ├── posts/ # Example feature: api.ts, queries.ts, screens, components, tests
│ │ └── settings/
│ ├── components/ # Shared UI: AppText, Button, Screen, Loading/Error/Empty states
│ ├── lib/api/ # request() and ApiError
│ ├── config/env.ts # Validated EXPO_PUBLIC_* config
│ ├── state/ # zustand stores
│ ├── theme/ # Tokens + useAppTheme()
│ ├── providers/ # QueryClient and other app-wide providers
│ └── test/ # Jest setup and render helpers
├── .env.example
├── .nvmrc
├── eslint.config.js
├── jest.config.js
├── tsconfig.json
└── package.json
Data flow for a screen: route file (src/app) → feature screen → TanStack Query hook
(queries.ts) → API function (api.ts) → request() → fetch, with the response validated by a
zod schema. Errors surface as ApiError and are shown with toUserMessage().
- Create
src/features/<name>/withapi.ts,queries.ts,<Name>Screen.tsx,__tests__/. Copy the shape ofsrc/features/posts/. - Add a route file in
src/app/that renders the screen. npm run check.
In Claude Code, /new-feature <name> performs these steps.
This template uses Expo's
Continuous Native Generation:
ios/ and android/ are generated from app.config.ts and its plugins by npm run prebuild,
npm run ios, npm run android, or EAS Build, and they are gitignored. All native build settings
the template needs (bundle IDs, icons, splash, URL scheme, variants) are declared in
app.config.ts. CI regenerates both projects on every push to catch config errors.
Reasons: renaming and SDK upgrades become config edits instead of Xcode/Gradle diffs, and the generated projects cannot drift from the config.
If you need to edit native code directly (custom native modules without a config plugin, vendor SDKs that require manual project edits):
- Run
npm run prebuild. - Remove
/iosand/androidfrom.gitignoreand commit both folders. - From then on,
app.config.tschanges to native settings no longer apply automatically. Make them in Xcode/Gradle, or re-run prebuild and re-apply your edits. - Update
AGENTS.md("Native code and configuration") and.claude/rules/native-config.md.
| Where | What | When it is read |
|---|---|---|
app.config.ts APP |
Name, slug, scheme, bundle ID, Android package, EAS IDs | Build time |
APP_VARIANT env var |
development / preview / production |
Build time |
.env (EXPO_PUBLIC_*) |
Runtime config (API base URL), validated in src/config/env.ts |
Inlined at bundle time |
EXPO_PUBLIC_*values are compiled into the JavaScript bundle and are readable by anyone with the app. Do not store secrets in them. Server-side secrets belong on your backend.- To add a variable: add it to
.env.example, to the zod schema and the object literal insrc/config/env.ts(Expo only inlines literalprocess.env.EXPO_PUBLIC_Xaccesses), and tosrc/test/setup.tsif tests need it. For EAS builds, set it as an EAS environment variable. - Variants:
developmentandpreviewappend.dev/.previewto the bundle ID and(Dev)/(Preview)to the name so they install alongside production.eas.jsonsetsAPP_VARIANTper profile. Reference: docs.expo.dev/build-reference/variants.
Release builds use EAS (eas.json). One-time setup:
npx eas-cli@latest login
npx eas-cli@latest init # creates the EAS project; copy projectId and owner into app.config.ts APPThen:
npx eas-cli@latest build --profile development --platform all # dev client for testers
npx eas-cli@latest build --profile preview --platform all # internal distribution
npx eas-cli@latest build --profile production --platform all # store builds
npx eas-cli@latest submit --platform ios|android # upload to storeseas.json uses appVersionSource: remote, so EAS manages build numbers; version in
app.config.ts is the user-facing version. Signing credentials are managed by EAS by default and
must never be committed (.gitignore excludes keystores, .p8, .p12 and provisioning profiles).
Local release builds without EAS are possible after npm run prebuild with Xcode (Archive) and
Gradle (./gradlew bundleRelease); you then manage signing yourself.
.github/workflows/ci.yml runs on pull requests and pushes to main:
checks:npm ci, typecheck, lint, format check, Jest,expo-doctor.native-config:expo prebuildandexpo export(JS bundle for iOS and Android) for thedevelopmentandproductionvariants on Ubuntu. This validatesapp.config.ts, plugins, and Metro bundling. It does not compile the native apps; that needs macOS runners with Xcode (iOS) or the Android SDK. Add EAS builds or platform build jobs when you are ready to pay for them.
Permissions are contents: read. No secrets are used.
Files Claude Code reads in this repository. Conventions were checked against the Claude Code docs on 2026-10-16 (.claude directory, memory, settings, permissions, hooks, skills, subagents).
| Path | Purpose | Committed |
|---|---|---|
AGENTS.md |
Project instructions in the cross-tool format: commands, layout, conventions, security rules. Edit this file for instructions every agent should follow. | yes |
CLAUDE.md |
Loaded at the start of every Claude Code session. Imports AGENTS.md with @AGENTS.md, then adds Claude-specific notes. When both files exist Claude Code reads CLAUDE.md, so the import is required. |
yes |
CLAUDE.local.md |
Personal project notes. Create it yourself if needed. | no |
.claude/settings.json |
Shared settings: permission allow/ask/deny rules, the format hook, and the Expo plugin. | yes |
.claude/settings.local.json |
Personal overrides; takes precedence over settings.json. |
no |
.claude/hooks/format-file.mjs |
PostToolUse hook: runs Prettier on every file Claude edits or writes. Reports syntax errors back to Claude. | yes |
.claude/rules/*.md |
Path-scoped instructions (paths: frontmatter) loaded only when Claude works on matching files: routes.md, testing.md, native-config.md. |
yes |
.claude/agents/code-reviewer.md |
Subagent: read-only review of the current diff against repo conventions. | yes |
.claude/agents/test-writer.md |
Subagent: writes or repairs Jest tests. | yes |
.claude/skills/new-feature/SKILL.md |
/new-feature <name>: scaffolds a feature folder, route and tests. |
yes |
.claude/skills/verify/SKILL.md |
/verify: runs all checks and reports PASS/FAIL per check. |
yes |
.claude/skills/rename-app/SKILL.md |
/rename-app: guided rename using scripts/rename.mjs. Manual-only (disable-model-invocation). |
yes |
Notes:
- Slash commands are implemented as skills (
.claude/skills/<name>/SKILL.md). The docs state that custom commands have been merged into skills;.claude/commands/<name>.mdfiles still work and create the same/name, but skills are recommended for new work. Add a command file there if you prefer the single-file format. - Permissions: check commands run without prompts; dependency installs, prebuild, EAS commands and
git pushalways ask;.envfiles and signing material are unreadable by Claude's file tools. Deny rules onReaddo not stop every shell form of reading a file; they are a guard, not a security boundary. enabledPluginsenablesexpo@claude-plugins-official, the Expo plugin that Expo's own SDK 58 default template enables. Remove the entry if you do not want it..mcp.json(project MCP servers) is not included. Add one if your team uses MCP servers.- The hook needs
nodeonPATHandnpm cialready run; it does nothing otherwise.
Run the script first, then work through the rest. npm run rename -- --check exits non-zero while
template identity values remain.
npm run rename -- --name "Acme Notes" --slug acme-notes --bundle-id com.acme.notes --dry-run
npm run rename -- --name "Acme Notes" --slug acme-notes --bundle-id com.acme.notes
# optional: --android-package com.acme.notes_app --scheme acmenotesThe script validates the values (Android packages cannot contain hyphens or Java keywords) and
updates app.config.ts, package.json and package-lock.json.
- Run
npm run rename(display name, slug, iOS bundle ID, Android package, URL scheme). - Replace artwork in
assets/images/(sizes inassets/images/README.md) and setSPLASH_BACKGROUND/ADAPTIVE_ICON_BACKGROUNDinapp.config.ts. - Replace the palette in
src/theme/tokens.ts. - Set
EXPO_PUBLIC_API_BASE_URLin.env.example, your.env, and EAS environment variables. - Replace or delete the example
src/features/posts/feature and its routesrc/app/posts/, and update the tabs insrc/app/(tabs)/_layout.tsx. - Run
npx eas-cli@latest initand filleasProjectIdandownerinapp.config.ts. - Set
versioninapp.config.ts. - Update the title and description at the top of this README.
- Choose a license and update
LICENSE(the template's MIT license has a placeholder holder). - Update
AGENTS.mdwith your product's domain terms and any decisions from below. - If
ios/orandroid/exist locally:npm run prebuild. -
npm run checkandnpm run doctor. - Mark the GitHub repo as a template only if you are publishing your own template.
The template makes no choice for these. Record decisions in AGENTS.md.
| Decision | Template state | Notes |
|---|---|---|
| Backend / API | JSONPlaceholder demo API for the example feature only | Replace EXPO_PUBLIC_API_BASE_URL and the posts schemas. |
| Authentication | None | Add auth headers in src/lib/api/client.ts. Store tokens with expo-secure-store, not AsyncStorage. |
| Client state library | zustand (one small persisted store) | Replaceable; only src/state/ and useAppTheme depend on it. |
| Server state library | TanStack Query | Widely used with React Native; handles caching, retries, cancellation. |
| Committed native folders | No (CNG) | See Native folders. |
| Crash reporting / analytics | None | Choose a vendor; most provide an Expo config plugin. |
| Internationalization | None; strings are inline English | Options: expo-localization plus an i18n library. |
| Over-the-air updates | Not configured | expo-updates + EAS Update, if you want JS updates without store review. |
| End-to-end tests | None | Maestro or Detox; both need built apps, so they belong in a separate CI job. |
| Web target | Not supported | react-dom is installed only because Expo Router declares it as a peer dependency. |
| Tablet support | iOS supportsTablet: true, portrait only |
Adjust orientation and ios.supportsTablet in app.config.ts. |
| Package manager | npm | Switching needs a new lockfile and CI cache change. |
The following is the original authoring verification record (2026-10-16, Linux, Node 22.19, npm 10.9), rather than a live CI status. Run the checks in your own environment:
- Executed and passing:
npm run typecheck(also with Expo Router's generated typed routes),npm run lint,npm run format:check,npm test(23 tests),npx expo-doctor(20/20),npx expo configfor each variant,npx expo prebuild --no-installgenerating bothios/andandroid/with the placeholder identifiers, icons and splash,npx expo exportproducing iOS and Android Hermes bundles,scripts/rename.mjs(dry run, validation errors, and a real run on a copy), and the Prettier hook (sample inputs piped to it, and triggered in a live Claude Code session while the template was written). - Not run: compiling or launching the app on iOS or Android (no Xcode or Android SDK in the
authoring environment), EAS builds, the GitHub Actions workflow on GitHub, the subagents and skills
inside a Claude Code session, and the
expo@claude-plugins-officialplugin install. - Run
npm run iosandnpm run androidonce after cloning to confirm your toolchain.
Read the contribution guide for the development workflow, testing expectations, and pull request checklist. The support guide covers troubleshooting and choosing the right issue form.
Distributed under the MIT License. See LICENSE for details.