How to Set Up Secrets for a Monorepo in 5 Minutes
Monorepos make sharing code easy but sharing secrets hard. Here's how to set up per-app, per-environment secrets with secr — without duplicating values or leaking across boundaries.
Monorepos solve code sharing. They don't solve secret sharing.
A typical monorepo looks like this:
apps/
web/ # Next.js frontend
api/ # Express/Hono backend
worker/ # Background job processor
packages/
shared/ # Shared types and utils
db/ # Database client
Each app needs its own secrets. The web app needs NEXT_PUBLIC_STRIPE_KEY. The API needs DATABASE_URL and STRIPE_SECRET_KEY. The worker needs REDIS_URL and QUEUE_SECRET. Some secrets are shared (the database URL), others are app-specific.
Most teams solve this with multiple .env files:
apps/web/.env.local
apps/api/.env
apps/worker/.env
This works until someone needs to update the database password and has to touch three files. Or until a new developer joins and needs all three files from Slack.
Here's how to do it properly with secr.
Step 1: Create the project (30 seconds)
npm install -g @secr/cli
secr login
Create a project for your monorepo:
secr init
# Select your org
# Create project: "my-monorepo"
# Select environment: "development"
This creates a .secr.json in your repo root. Commit it — it contains no secrets.
Step 2: Set up environments (1 minute)
secr comes with development, staging, and production environments by default. Each environment has its own set of secrets. Nothing is shared unless you explicitly promote values between them.
# Development secrets
secr set DATABASE_URL "postgres://localhost:5432/myapp" --env development
secr set REDIS_URL "redis://localhost:6379" --env development
secr set STRIPE_SECRET_KEY "sk_test_..." --env development
secr set NEXT_PUBLIC_STRIPE_KEY "pk_test_..." --env development
# Production secrets
secr set DATABASE_URL "postgres://prod-host/myapp" --env production
secr set REDIS_URL "redis://prod-redis:6379" --env production
secr set STRIPE_SECRET_KEY "sk_live_..." --env production
secr set NEXT_PUBLIC_STRIPE_KEY "pk_live_..." --env production
Step 3: Run each app with its secrets (1 minute)
The key insight: all apps in the monorepo share the same secr project and environment. Each app just uses the variables it needs.
Using Turborepo
If you're using Turborepo, wrap each dev command with secr run:
{
"scripts": {
"dev": "secr run -- turbo dev",
"dev:web": "secr run -- turbo dev --filter=web",
"dev:api": "secr run -- turbo dev --filter=api",
"dev:worker": "secr run -- turbo dev --filter=worker"
}
}
secr run injects all secrets as environment variables. Each app reads only the variables it cares about — the rest are ignored.
Using npm workspaces
Same idea without Turborepo:
{
"scripts": {
"dev:web": "secr run -- npm --workspace=apps/web run dev",
"dev:api": "secr run -- npm --workspace=apps/api run dev",
"dev:worker": "secr run -- npm --workspace=apps/worker run dev"
}
}
Using pnpm
{
"scripts": {
"dev": "secr run -- pnpm --parallel -r run dev"
}
}
Step 4: Handle app-specific secrets (1 minute)
Some secrets only make sense for one app. Use a naming convention to keep things organised:
# API-only
secr set API_JWT_SECRET "your-jwt-secret"
secr set API_RATE_LIMIT_KEY "rl-..."
# Worker-only
secr set WORKER_QUEUE_SECRET "qs-..."
secr set WORKER_BATCH_SIZE "100"
# Web-only (client-exposed)
secr set NEXT_PUBLIC_POSTHOG_KEY "phk-..."
secr set NEXT_PUBLIC_API_URL "https://api.myapp.com"
Use secr ls to see everything in one place:
$ secr ls
my-monorepo/development — 11 secrets
API_JWT_SECRET v1
API_RATE_LIMIT_KEY v1
DATABASE_URL v1
NEXT_PUBLIC_API_URL v1
NEXT_PUBLIC_POSTHOG_KEY v1
NEXT_PUBLIC_STRIPE_KEY v1
REDIS_URL v1
STRIPE_SECRET_KEY v1
WORKER_BATCH_SIZE v1
WORKER_QUEUE_SECRET v1
Use descriptions to document which app uses each secret:
secr set API_JWT_SECRET "..." --description "Used by apps/api for JWT signing"
secr set WORKER_QUEUE_SECRET "..." --description "Used by apps/worker for queue auth"
Step 5: Deploy (1 minute)
Vercel (for the web app)
Set SECR_TOKEN in Vercel's environment variables, then use the build command:
npx secr-vercel && next build
Railway / Render / Fly (for API and worker)
Set SECR_TOKEN and use secr run in your start command:
CMD ["npx", "secr", "run", "--", "node", "dist/server.js"]
Or in your Procfile:
web: secr run -- node apps/api/dist/server.js
worker: secr run -- node apps/worker/dist/index.js
GitHub Actions
Use secr in your CI pipeline:
- name: Run tests with secrets
run: secr run --env staging -- npm test
env:
SECR_TOKEN: ${{ secrets.SECR_TOKEN }}
The result
| Before (multiple .env files) | After (secr) |
|---|---|
| 3 separate .env files | 1 project, 1 secr run command |
| Update DATABASE_URL in 3 places | Update once with secr set |
| New developer needs 3 files from Slack | secr login && secr run |
| No idea which secrets are current | secr ls shows everything |
| Different secrets per machine | Everyone gets the same set |
| No audit trail | Full audit log |
Alternative: separate projects per app
If your apps have completely independent secret sets and different teams manage them, you can create separate secr projects:
secr init # in apps/web — project: "my-web"
secr init # in apps/api — project: "my-api"
secr init # in apps/worker — project: "my-worker"
Each app gets its own .secr.json and its own secret namespace. This is better when teams don't overlap and secrets are truly independent.
For most monorepos, a single project is simpler.
Running a monorepo? Set up secr and stop juggling .env files across apps.
Ready to get started?
Stop sharing secrets over Slack. Get set up in under two minutes.
Create your account