Skip to content

Commit acbc041

Browse files
committed
feat: centralize env handling with root env.js, add app.config.ts and client Env bridge
1 parent 9f99175 commit acbc041

File tree

14 files changed

+520
-183
lines changed

14 files changed

+520
-183
lines changed

README.md

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ To create a new project using this boilerplate template:
2525
git commit -m "Initial commit"
2626
```
2727

28-
2. **Update project details**: Modify the `app.json` file to reflect your project's name, slug, and other configuration
29-
details.
28+
2. **Update project details**:
29+
- Open `env.js` and update the template values:
30+
- `BUNDLE_ID`: Your iOS Bundle ID / Android Package Name (e.g., `com.yourcompany.yourapp`)
31+
- `NAME`: Your app's display name (e.g., `My Awesome App`)
32+
- `SLUG`: Your Expo slug (e.g., `my-awesome-app`)
33+
- `EXPO_ACCOUNT_OWNER`: Your Expo account username
34+
- `EAS_PROJECT_ID`: Get by running `npx eas init`
35+
- `SCHEME`: Your app's URL scheme for deep links (e.g., `myapp`)
3036

3137
3. **Install dependencies**:
3238

@@ -219,90 +225,148 @@ The logger automatically:
219225

220226
## Environment Variables
221227

222-
This project uses a centralized, type-safe environment variable system with Zod validation.
228+
This project uses a centralized, type-safe environment variable system with Zod validation, managed through `env.js` and
229+
`app.config.ts`.
230+
231+
### Architecture
232+
233+
- **`env.js`** (root): Loads and validates environment variables, manages app configuration
234+
- **`app.config.ts`**: TypeScript config that uses validated variables from `env.js`
235+
- **`src/env.js`**: Client-side access point - imports via `@/env` in your code
236+
- **`.env.{environment}`**: Environment-specific variables (development, staging, production)
223237

224238
### Configuration
225239

226-
1. **Copy the environment template**:
240+
1. **Update app configuration** in `env.js`:
241+
242+
Follow the template instructions to set your:
243+
- Bundle ID, app name, slug
244+
- Expo account owner and EAS project ID
245+
- URL scheme for deep links
246+
247+
2. **Create environment file**:
227248

228249
```bash
229-
cp .env.example .env.local
250+
cp .env.example .env.development
230251
```
231252

232-
2. **Configure your variables** in `.env.local`:
253+
3. **Configure your variables** in `.env.development`:
233254

234255
```bash
235256
# API Configuration
236257
EXPO_PUBLIC_API_URL=https://your-api.com
237258
EXPO_PUBLIC_DEFAULT_LOCALE=en
238259

239-
# MMKV Encryption (32+ characters required)
260+
# MMKV Encryption (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
240261
EXPO_PUBLIC_AUTH_STORAGE_ENCRYPTION_KEY=your-secure-random-64-character-hex-key
241262

242263
# Sentry (optional)
264+
EXPO_PUBLIC_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
243265
SENTRY_ORG=your-sentry-org
244266
SENTRY_PROJECT=your-project
245267
SENTRY_AUTH_TOKEN=your-token
246268
```
247269

248-
3. **Start the app** - environment variables are automatically validated on startup
270+
4. **Start the app** - environment variables are automatically validated on startup
271+
272+
### Multiple Environments
273+
274+
The project supports multiple app variants on the same device:
275+
276+
- **Development**: `.env.development``yourapp-dev://` → "YourApp (Dev)"
277+
- **Staging**: `.env.staging``yourapp-staging://` → "YourApp (Staging)"
278+
- **Production**: `.env.production``yourapp://` → "YourApp"
279+
280+
Switch environments:
281+
282+
```bash
283+
APP_ENV=staging npm start
284+
APP_ENV=production npm start
285+
```
249286

250287
### Usage in Code
251288

252-
**Always import from `@/env` instead of using `process.env` directly:**
289+
**Always import from `@/env` - ESLint enforces this:**
253290

254291
```typescript
255-
// ✅ Correct
292+
// ✅ Correct - Import from @/env
256293
import { env } from '@/env';
257294

258295
const apiUrl = env.EXPO_PUBLIC_API_URL;
259296
const locale = env.EXPO_PUBLIC_DEFAULT_LOCALE;
297+
const appName = env.NAME; // Dynamically generated based on APP_ENV
298+
const scheme = env.SCHEME; // Environment-specific URL scheme
260299

261-
// ❌ Wrong - ESLint will prevent this
300+
// ❌ Wrong - Direct process.env access (ESLint error)
262301
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
302+
303+
// ❌ Wrong - Importing root env.js (ESLint error)
304+
import { ClientEnv } from '../../env';
305+
306+
// ✅ Platform detection - Use Platform.OS instead
307+
import { Platform } from 'react-native';
308+
if (Platform.OS === 'ios') {
309+
/* ... */
310+
}
263311
```
264312

265313
### Adding New Variables
266314

267315
**For client-side variables** (accessible in the app):
268316

269-
1. Add the variable to `.env.local` with `EXPO_PUBLIC_` prefix
270-
2. Update the schema in `src/env.ts`:
271-
```typescript
272-
const clientEnvSchema = z.object({
317+
1. Add to `.env.development` with `EXPO_PUBLIC_` prefix:
318+
319+
```bash
320+
EXPO_PUBLIC_YOUR_NEW_VAR=your-value
321+
```
322+
323+
2. Update the schema in `env.js`:
324+
325+
```javascript
326+
const client = z.object({
273327
// ... existing vars
274-
EXPO_PUBLIC_YOUR_NEW_VAR: z.string().min(1, 'EXPO_PUBLIC_YOUR_NEW_VAR is required'),
328+
EXPO_PUBLIC_YOUR_NEW_VAR: z.string().optional(), // or .min(1) for required
275329
});
276330
```
277-
3. Add explicit reference in `validateClientEnv()`:
278-
```typescript
279-
const envVars = {
331+
332+
3. Add to `_clientEnv` object:
333+
334+
```javascript
335+
const _clientEnv = {
280336
// ... existing vars
281337
EXPO_PUBLIC_YOUR_NEW_VAR: process.env.EXPO_PUBLIC_YOUR_NEW_VAR,
282338
};
283339
```
284-
4. Restart the app - validation will run automatically
285340

286-
**For server-side variables** (build-time/CI only):
341+
4. Restart with clear cache:
342+
```bash
343+
npm start -- --clear
344+
```
345+
346+
**For build-time variables** (app.config.ts only):
287347

288-
1. Add the variable to `.env.local` (no `EXPO_PUBLIC_` prefix)
289-
2. Access directly via `process.env.YOUR_VAR` in scripts or config files
290-
3. Not validated or accessible in client code
348+
1. Add to `.env.development` (no `EXPO_PUBLIC_` prefix)
349+
2. Update `buildTime` schema in `env.js`
350+
3. Add to `_buildTimeEnv` object
351+
4. Access via `Env` in `app.config.ts` only
291352

292353
### Security Notes
293354

294-
- `.env.local` is git-ignored and never committed
295-
- **Client-side variables** (prefixed with `EXPO_PUBLIC_`):
296-
- Validated on app startup via `src/env.ts`
297-
- Embedded in the app bundle and accessible at runtime
298-
- Used for: API URLs, default locale, MMKV encryption key, Sentry DSN
299-
- **Must have `EXPO_PUBLIC_` prefix** to be inlined by Metro bundler
300-
- **Server-side variables** (no prefix):
301-
- NOT validated in `src/env.ts` to avoid build failures
302-
- Only available during build-time, CI, and in Node.js scripts
303-
- Used for: Sentry build tokens, build configuration
304-
- NOT accessible in client code
305-
- ESLint enforces centralized env usage (no direct `process.env` access in src/)
355+
- `.env.*` files are git-ignored and never committed (only `.env.example` is tracked)
356+
- **Client-side variables** (`EXPO_PUBLIC_` prefix):
357+
- Validated via Zod schema in `env.js`
358+
- Embedded in app bundle - accessible at runtime
359+
- Passed to app via `app.config.ts``extra` field → `@/env`
360+
- Examples: API URLs, locale, MMKV key, Sentry DSN
361+
- **Build-time variables** (no prefix):
362+
- Only available in `app.config.ts` and build scripts
363+
- Not embedded in app bundle
364+
- Examples: Sentry auth token, EAS project ID
365+
- **ESLint enforcement**:
366+
- ✅ Allows: `import { env } from '@/env'`
367+
- ❌ Blocks: `process.env.*` in `src/`
368+
- ❌ Blocks: Relative imports to root `env.js`
369+
- ✅ Exception: `Platform.OS` for platform detection
306370

307371
## Learn more
308372

app.config.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { ConfigContext, ExpoConfig } from 'expo/config';
2+
3+
import { ClientEnv, Env } from './env';
4+
5+
/**
6+
* Expo App Configuration
7+
*
8+
* This config uses TypeScript for better type safety and IDE support.
9+
* Environment variables are loaded and validated via env.js
10+
*
11+
* Docs: https://docs.expo.dev/workflow/configuration/
12+
*/
13+
export default ({ config }: ConfigContext): ExpoConfig => ({
14+
...config,
15+
name: Env.NAME,
16+
slug: Env.SLUG,
17+
version: Env.VERSION,
18+
orientation: 'portrait',
19+
icon: './src/assets/images/icon.png',
20+
scheme: Env.SCHEME,
21+
userInterfaceStyle: 'automatic',
22+
owner: Env.EXPO_ACCOUNT_OWNER,
23+
ios: {
24+
supportsTablet: true,
25+
bundleIdentifier: Env.BUNDLE_ID,
26+
},
27+
android: {
28+
adaptiveIcon: {
29+
backgroundColor: '#E6F4FE',
30+
foregroundImage: './src/assets/images/android-icon-foreground.png',
31+
backgroundImage: './src/assets/images/android-icon-background.png',
32+
monochromeImage: './src/assets/images/android-icon-monochrome.png',
33+
},
34+
edgeToEdgeEnabled: true,
35+
predictiveBackGestureEnabled: false,
36+
package: Env.PACKAGE,
37+
},
38+
web: {
39+
output: 'static',
40+
favicon: './src/assets/images/favicon.png',
41+
},
42+
plugins: [
43+
'expo-router',
44+
[
45+
'expo-splash-screen',
46+
{
47+
image: './src/assets/images/splash-icon.png',
48+
imageWidth: 200,
49+
resizeMode: 'contain',
50+
backgroundColor: '#ffffff',
51+
dark: {
52+
backgroundColor: '#000000',
53+
},
54+
},
55+
],
56+
[
57+
'@sentry/react-native/expo',
58+
{
59+
url: 'https://sentry.io/',
60+
note: 'Sentry configuration will be loaded from environment variables: SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN',
61+
},
62+
],
63+
'react-native-edge-to-edge',
64+
],
65+
experiments: {
66+
typedRoutes: true,
67+
reactCompiler: true,
68+
},
69+
extra: {
70+
// Pass client environment variables to the app
71+
// These will be accessible via expo-constants in src/
72+
...ClientEnv,
73+
eas: {
74+
projectId: Env.EAS_PROJECT_ID,
75+
},
76+
},
77+
});

app.json

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)