tutorialmonoreposecrets-managementdevops

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.

secr team·

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 files1 project, 1 secr run command
Update DATABASE_URL in 3 placesUpdate once with secr set
New developer needs 3 files from Slacksecr login && secr run
No idea which secrets are currentsecr ls shows everything
Different secrets per machineEveryone gets the same set
No audit trailFull 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