Create or Clone a Project

Starting a new project

pnpm create expo-app my-app        # or: npx create-expo-app@latest my-app
cd my-app && pnpm install

This gives you a modern Expo app (SDK 54 / RN 0.81 style) with TypeScript and file-based routing ready to go.

Joining an existing project

git clone <repo-url>
cd <repo>
pnpm install

A note on native folders

The team default is the managed workflow with Continuous Native Generation (CNG): the ios/ and android/ folders are never committed. They’re generated on demand — locally with npx expo prebuild, or transparently by EAS Build — from app.config.ts and your installed packages. See Getting Started → Managed Workflow (CNG) for the full rationale and what to do when a native SDK needs more than a config plugin provides.

Git hygiene: your .gitignore

create-expo-app ships a solid .gitignore for the managed workflow — this guide’s default below builds on it by ignoring /ios and /android outright, not just their build subfolders. If a project deviates from that default and commits the native folders anyway, it also inherits their build outputs, which are easy to miss and bloat every clone with hundreds of MB of compiled artifacts.

Builds never belong in the repo. Compiled apps (*.apk, *.aab, *.ipa) and dependency caches (node_modules/) are all generated — regenerated from source on every build. Committing them bloats history irreversibly.

Make sure your .gitignore covers at least the following:

# Dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native folders — generated by CNG, never commit (see Managed Workflow)
/ios
/android

# Compiled app binaries — generated on every build
*.apk
*.aab
*.ipa
*.app
*.dSYM
*.dSYM.zip

# Metro / bundler
.metro-health-check*
*.jsbundle

# Signing material & secrets — must never be committed
*.p8
*.p12
*.key
*.mobileprovision
.env
.env.*
!.env.example

# Logs & debug output
npm-debug.*
yarn-debug.*
yarn-error.*
*.log

# TypeScript
*.tsbuildinfo

# IDE / OS noise
.DS_Store
.vscode/
.idea/
*.pem

A few notes on the entries that trip people up:

  • No Android debug keystore to commit. Under the managed default, android/ isn’t committed at all, so there’s no shared debug.keystore file to carve an exception for. Each machine (and EAS) uses its own debug signature; if a feature genuinely needs a shared debug SHA-1 (e.g. Google Sign-In), manage it as an EAS credential, not a committed file. Your release keystore is always a secret regardless of workflow: keep it out of the repo (store it in EAS or a secrets manager).
  • The .env.* files (.env.dev, .env.preview, .env.prod) are ignored; .env.example is committed. See Environment Variables and Environment & Configuration → Secrets for the full rule — and rotate anything that leaks into git history.
  • Firebase config (google-services.json, GoogleService-Info.plist): these can’t live inside ios//android/ under the managed default (not committed), so point to them from outside — e.g. a root-level path referenced via googleServicesFile in app.config.ts, which a config plugin copies into the generated project at prebuild time. Whether that file itself is safe to commit (it’s usually a non-secret per-project identifier) is a decision to make once per project and document in the repo’s README.

If ios/, android/, or a build artifact is already tracked, adding it to .gitignore won’t remove it — Git keeps ignoring changes only to untracked files. Stop tracking it with git rm -r --cached ios android (adjust paths for a narrower cleanup), then commit.

Next: set up your Environment Variables.