Monorepo Patterns
Hush is designed specifically for monorepos where different packages need different subsets of your environment variables.
The Problem
In a typical monorepo, you might have:
Directorypackages/
Directorymobile/ # Expo app - needs EXPO_PUBLIC_* vars
- …
Directoryweb/ # Next.js - needs NEXT_PUBLIC_* vars
- …
Directoryapi/ # Cloudflare Worker - needs API keys
- …
Directoryshared/ # Shared library - might need some config
- …
- .env # All your secrets in one place
Each package needs different variables:
- Mobile app: Only client-safe
EXPO_PUBLIC_*variables - Web app: Only client-safe
NEXT_PUBLIC_*variables - API: Server secrets, but not client variables
- Root: Everything, for scripts and local development
Manually copying .env files is error-prone. Committing different subsets is a maintenance nightmare.
The Solution
Define routing rules once in hush.yaml:
sources: shared: .env development: .env.development production: .env.production
targets: - name: root path: . format: dotenv
- name: mobile path: ./packages/mobile format: dotenv include: - EXPO_PUBLIC_*
- name: web path: ./packages/web format: dotenv include: - NEXT_PUBLIC_*
- name: api path: ./packages/api format: wrangler exclude: - EXPO_PUBLIC_* - NEXT_PUBLIC_*One hush decrypt command distributes the right secrets to every package.
Common Patterns
Client vs Server Split
The most common pattern - keep client-safe variables separate from server secrets:
targets: # Client apps get public variables only - name: app path: ./packages/app format: dotenv include: - EXPO_PUBLIC_* - NEXT_PUBLIC_* - VITE_*
# Server gets everything except public variables - name: api path: ./packages/api format: wrangler exclude: - EXPO_PUBLIC_* - NEXT_PUBLIC_* - VITE_*Multiple APIs
When you have multiple backend services:
targets: - name: auth-api path: ./packages/auth format: wrangler include: - AUTH_* - JWT_* - DATABASE_*
- name: payments-api path: ./packages/payments format: wrangler include: - STRIPE_* - DATABASE_*Shared Configuration
Some variables need to be everywhere:
targets: # Every package gets APP_* variables - name: mobile path: ./packages/mobile format: dotenv include: - APP_* - EXPO_PUBLIC_*
- name: web path: ./packages/web format: dotenv include: - APP_* - NEXT_PUBLIC_*
- name: api path: ./packages/api format: wrangler include: - APP_* - API_* - DATABASE_*Environment-Specific URLs
Use interpolation for environment-specific base URLs:
# .env (shared)EXPO_PUBLIC_API_URL=${API_BASE}/v1
API_BASE=http://localhost:8787
# .env.productionAPI_BASE=https://api.yourapp.comThis gives you:
- Development:
EXPO_PUBLIC_API_URL=http://localhost:8787/v1 - Production:
EXPO_PUBLIC_API_URL=https://api.yourapp.com/v1
Real-World Example
Here’s a complete example for a typical full-stack monorepo:
Directorypackages/
Directorymobile/ # Expo React Native app
- …
Directoryweb/ # Next.js marketing site
- …
Directorydashboard/ # Vite admin dashboard
- …
Directoryapi/ # Cloudflare Worker API
- …
Directoryshared/ # Shared TypeScript library
- …
- .env
- .env.development
- .env.production
- hush.yaml
Environment Files
# .env (shared secrets)DATABASE_URL=postgres://user:pass@host/dbSTRIPE_SECRET_KEY=sk_xxxSTRIPE_WEBHOOK_SECRET=whsec_xxxSENDGRID_API_KEY=SG.xxx
# API URLs (interpolated)EXPO_PUBLIC_API_URL=${API_BASE}/v1NEXT_PUBLIC_API_URL=${API_BASE}/v1VITE_API_URL=${API_BASE}/v1
# App configAPP_NAME=MyAppAPP_VERSION=1.0.0API_BASE=http://localhost:8787EXPO_PUBLIC_ENV=developmentNEXT_PUBLIC_ENV=developmentVITE_ENV=developmentDEBUG=trueAPI_BASE=https://api.myapp.comEXPO_PUBLIC_ENV=productionNEXT_PUBLIC_ENV=productionVITE_ENV=productionDEBUG=falseConfiguration
sources: shared: .env development: .env.development production: .env.production
targets: # Root for scripts - name: root path: . format: dotenv
# Expo mobile app - name: mobile path: ./packages/mobile format: dotenv include: - EXPO_PUBLIC_* - APP_NAME - APP_VERSION
# Next.js marketing site - name: web path: ./packages/web format: dotenv include: - NEXT_PUBLIC_* - APP_NAME
# Vite admin dashboard - name: dashboard path: ./packages/dashboard format: dotenv include: - VITE_* - APP_NAME
# Cloudflare Worker API - name: api path: ./packages/api format: wrangler exclude: - EXPO_PUBLIC_* - NEXT_PUBLIC_* - VITE_*Workflow
Development
# Decrypt for local developmenthush decrypt
# Each package now has its .env.development file# api/ has .dev.vars for wranglerDeployment
# Decrypt production secretshush decrypt -e production
# Push to Cloudflare Workershush pushCI/CD
In your GitHub Actions or CI pipeline:
- name: Decrypt secrets run: | echo "${{ secrets.AGE_SECRET_KEY }}" > $HOME/.config/sops/age/key.txt npx hush decrypt -e production
- name: Deploy API run: | cd packages/api wrangler deployVerifying Your Setup
Use hush status to verify your configuration:
hush statusThis shows:
- Which source files are configured
- Encryption status of each file
- Target distribution with their filters