A production-ready scaffold for building iOS applications with Expo, React Native, and Supabase. Designed to be easily renamed and customized for new projects.
- Frontend: Expo SDK 54 + React Native 0.81 + TypeScript
- Styling: NativeWind v4 (Tailwind CSS for React Native)
- State Management: React Query with offline persistence
- Backend: Supabase (Auth, Database, Storage) + Express API on AWS Lambda
- Navigation: Expo Router (file-based routing)
- Build: EAS Build with three environments (dev, preview, prod)
- Testing: Jest + React Native Testing Library
- Node.js 18+
- npm or yarn
- Expo CLI (
npm install -g expo-cli) - EAS CLI (
npm install -g eas-cli) - Supabase account
- AWS account (for backend deployment)
# Clone the repository
git clone <your-repo-url>
cd expo-app-scaffold
# Install dependencies
make install
# Set up environment variables
cp .env.example .env
# Edit .env with your values-
Update
.envwith your app name, bundle ID, and credentials:EXPO_PUBLIC_APP_NAME="Your App Name" EXPO_PUBLIC_APP_SLUG="your-app-slug" EXPO_PUBLIC_BUNDLE_ID="com.yourcompany.yourapp"
-
Initialize EAS (after creating an Expo account):
eas init # Add the project ID to .env as EXPO_PUBLIC_EAS_PROJECT_ID -
Add your assets in
assets/branding/:app-icon.png(1024x1024)splash.png(1284x2778)
# Start the Expo development server
make dev
# Start the backend server
make dev-backend
# Start iOS simulator
make ios
# Start Android emulator
make androidexpo-app-scaffold/
├── app/ # Expo Router screens
│ ├── (auth)/ # Authentication screens
│ ├── (tabs)/ # Main app tabs
│ ├── _layout.tsx # Root layout with providers
│ └── index.tsx # Entry point
│
├── lib/ # Shared library code
│ ├── api/ # HTTP client with interceptors
│ │ └── client.ts
│ │
│ ├── components/
│ │ ├── ErrorBoundary.tsx # Error boundary for crash recovery
│ │ └── ui/ # Reusable UI components
│ │
│ ├── constants/ # Design system tokens
│ │ ├── colors.ts
│ │ ├── typography.ts
│ │ ├── spacing.ts
│ │ ├── shadows.ts
│ │ └── animations.ts
│ │
│ ├── hooks/ # Custom React hooks
│ │
│ ├── providers/ # React context providers
│ │ ├── AuthProvider.tsx
│ │ ├── AlertProvider.tsx
│ │ └── QueryProvider.tsx
│ │
│ ├── services/ # External service clients
│ │ ├── supabase.ts
│ │ └── logger.ts
│ │
│ ├── query/ # React Query configuration
│ │ └── keys.ts # Query key factory
│ │
│ ├── types/ # TypeScript type definitions
│ │ ├── common.ts
│ │ └── database.ts # Auto-generated from Supabase
│ │
│ └── utils/ # Utility functions
│ ├── storage.ts # AsyncStorage + SecureStore wrapper
│ ├── date.ts # Date formatting helpers
│ └── validation.ts # Zod validation schemas
│
├── backend/ # Backend API
│ ├── src/
│ │ ├── index.ts # Express entry point
│ │ ├── middleware/ # Express middleware
│ │ └── routes/ # API routes
│ ├── serverless.yml # AWS Lambda config
│ └── package.json
│
├── __tests__/ # Test files
│ ├── components/
│ ├── utils/
│ └── test-utils/ # Test utilities
│
├── app.config.js # Expo configuration
├── babel.config.js # Babel configuration
├── metro.config.js # Metro bundler configuration
├── tailwind.config.js # NativeWind configuration
├── tsconfig.json # TypeScript configuration
├── jest.config.js # Jest configuration
├── eas.json # EAS Build configuration
└── Makefile # Build automation
The app includes an ErrorBoundary component that catches JavaScript errors:
// Automatically wrapped in root layout
// Custom error handling in app/_layout.tsxHTTP client with automatic token handling, retry logic, and timeout:
import { apiClient } from '@/lib/api';
// Automatic auth headers and retry
const { data } = await apiClient.get('/api/users');
await apiClient.post('/api/users', { name: 'John' });Form validation with Zod:
import { validate, loginSchema } from '@/lib/utils/validation';
const result = validate(loginSchema, { email, password });
if (!result.success) {
console.log(result.errors);
}Type-safe storage utilities:
import { storage, secureStorage, STORAGE_KEYS } from '@/lib/utils/storage';
// Regular storage
await storage.set('user', userData);
const user = await storage.get<User>('user');
// Secure storage for sensitive data
await secureStorage.set(STORAGE_KEYS.AUTH_TOKEN, token);import { formatDate, formatRelative, isToday } from '@/lib/utils/date';
formatDate(new Date(), 'short'); // "Jan 15, 2024"
formatRelative(pastDate); // "2 hours ago"
isToday(date); // true/falseEnvironment-aware logging:
import { logger } from '@/lib/services/logger';
logger.debug('Debug info'); // Only in development
logger.info('User action'); // Development + preview
logger.error('Error!', { details }); // All environmentsimport { COLORS } from '@/lib/constants/colors';
COLORS.primary // Main brand color
COLORS.error // Error states
COLORS.text // Primary text
COLORS.border // Bordersimport { Typography } from '@/lib/components/ui';
<Typography variant="h1">Heading</Typography>
<Typography variant="body" color="secondary">Text</Typography>import { SPACING } from '@/lib/constants/spacing';
SPACING.xs // 4
SPACING.sm // 8
SPACING.md // 12
SPACING.lg // 16# Run all tests
make test
# Run with coverage
make test-coverage
# Run in watch mode
make test-watchTest utilities are provided in __tests__/test-utils/:
import { render, screen } from '@/__tests__/test-utils/render';
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeTruthy();make build-dev # All platforms
make build-ios-dev # iOS only
make build-android-dev # Android onlymake build-previewmake build-prodmake deploy-backend-dev # Development
make deploy-backend-prod # Productionmake submit-ios # App Store Connect
make submit-android # Play Console| Variable | Description |
|---|---|
EXPO_PUBLIC_APP_NAME |
App display name |
EXPO_PUBLIC_APP_SLUG |
App slug (lowercase, no spaces) |
EXPO_PUBLIC_BUNDLE_ID |
Bundle identifier |
EXPO_PUBLIC_EAS_PROJECT_ID |
EAS project ID |
EXPO_PUBLIC_SUPABASE_URL |
Supabase project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY |
Supabase anonymous key |
EXPO_PUBLIC_API_URL |
Backend API URL |
- Use the Design System - Never hardcode colors, spacing, or typography
- Query Key Factory - Use
queryKeysfor consistent cache management - Error Handling - Use
useAlert()for user feedback - Loading States - Use
<Skeleton />components - Form Validation - Use Zod schemas from
lib/utils/validation - Type Safety - Generate database types with
make db-generate-types
MIT