Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

secrets manager forhumans & agents.

Login with GoogleLearn more
Mellon replaces .env files with a cloud-based secrets manager you can self-host. Prefix your commands with mellon run and secrets are injected as environment variables, never written to disk.
# instead of this source .env && next dev # do this mellon run -- next dev
┌────────────────┐ mellon run -- next dev │ App Worker │ │ │ (mellon.jsj.sh) │ │ 1. fetch secrets │ │ │──────────────────────────────────────▶│ decrypt │ │ { DB_URL, API_KEY, ... } │ AES-256-GCM │ │◀──────────────────────────────────────│ │ │ └────────────────┘ │ 2. spawn child with env vars │ ▼ ┌──────────────┐ │ next dev │ │ (child) │ └──────┬───────┘ │ │ 3. stdout / stderr ▼ ┌───────────────┐ │ redaction │ high-entropy values replaced with * │ filter │ secrets never reach your terminal └──────┬────────┘ │ ▼ terminal (safe output)
Secrets are automatically redacted from process output so they never leak into agent context windows, CI logs, or terminal history.
Open-source alternative to Doppler and Infisical.

Why Mellon?

Why not Doppler or Infisical?

  • Self-hosted: runs on your own Cloudflare account. No centralized point of failure, no vendor lock-in. Your secrets never leave infrastructure you control.
  • Free: no per-seat pricing, no usage limits. Deploy it once, use it forever.
  • Open source: MIT licensed. Read the code, audit it, extend it.

Why teams need this

  • No more .env files: secrets live in the cloud and are easy to share across machines. No more "can you send me the .env?" on Slack.
  • Single source of truth: stop duplicating secrets across platforms. In CI, you only need the Mellon token. Use built-in scripts to sync secrets to Cloudflare, Vercel, Docker, and more.
  • Collaborative secrets: share secrets between team members through organizations with role-based access, instead of brittle .env files or pasting keys in DMs.
  • Multi-environment management: manage dev, staging, and production secrets in one place. Switch between environments with -c production.

Why agents need this

  • Don't let agents read your secrets: agents should never see your raw secret values. Instead of giving agents access to .env files, use mellon run to inject secrets into processes without exposing them.
  • Automatic output redaction: mellon run replaces secret values in stdout/stderr with *, so secrets never enter your chat context window. Even if an agent runs printenv, it won't see the real values in the output.

Install skill for AI agents

npx -y skills add jsj/mellon
This installs skills for AI coding agents like Claude Code, Cursor, Windsurf, and others.

Install

curl (downloads the native binary to ~/.mellon/bin):
curl -fsSL https://mellon.jsj.sh/install.sh | bash
npm:
npm i -g mellon
Run without installing via npx or bunx:
npx mellon run -- next dev bunx mellon run -- next dev

Quick start

1. Add your secrets at mellon.jsj.sh (or your self-hosted instance). Create a project, add environments, and paste in your secrets from the web UI.
2. Login from the terminal (opens a browser for device flow authentication):
mellon login
3. Link your project (picks the default project and environment for this directory):
mellon setup
This saves the project and environment for the current directory in ~/.mellon/config.json (not in the repo). Run it in the project root if you have a single project, or in each subfolder of a monorepo. Since the config is local to your machine, you need to run mellon setup again after cloning the repo on a new machine. Alternatively, skip setup entirely and always pass --project and --env (or -c) flags.
To audit existing Doppler and Mellon consumers before setup or migration, run:
mellon discover --path ~/Developer
This read-only command finds local commands, GitHub Actions, Xcode Cloud scripts, and Cloudflare integrations. It reports identifiers and file locations without reporting secret values.
Preview a name-only Cloudflare Worker synchronization:
mellon sync cloudflare --project <project> --env <environment> --worker <worker-name>
Add --apply after you review the plan. Production environments also require --confirm-production.
4. Run your app with secrets injected as environment variables:
mellon run -- next dev
That's it. No .env files, no copy-pasting keys. Go back to mellon.jsj.sh any time to add, edit, or rotate secrets. The next mellon run picks them up automatically.
Migrating from Doppler? See the Doppler migration guide.

Setting up a new project

The Quick Start above assumes you already have a project with secrets. This section walks through creating everything from scratch, either from the CLI or the dashboard.
Mellon organizes secrets into a simple hierarchy:
Organization (my-company) │ ├── Project (api) │ ├── dev │ │ ├── DATABASE_URL = postgres://localhost/mydb │ │ ├── API_KEY = sk-dev-xxx │ │ └── AUTH_SECRET = random-dev-key │ ├── preview │ │ ├── DATABASE_URL = postgres://preview-host/mydb │ │ └── API_KEY = sk-preview-xxx │ └── prod │ ├── DATABASE_URL = postgres://prod-host/mydb │ └── API_KEY = sk-live-xxx │ └── Project (web) ├── dev │ └── NEXT_PUBLIC_API_URL = http://localhost:3001 └── prod └── NEXT_PUBLIC_API_URL = https://api.example.com
Each organization contains multiple projects. Each project has environments (dev, preview, prod by default). Secrets are scoped to a single environment.

Create an organization

Organizations group projects and team members together. You need one before creating any project.
CLI:
mellon orgs create --name my-company
Dashboard: Go to mellon.jsj.sh and click "Create Organization" from the sidebar.

Create a project

A project holds secrets for one app or service. Creating a project automatically gives you three environments: dev, preview, and prod.
mellon orgs # find your org ID mellon projects create --org <ORG_ID> --name my-app
Dashboard: Open your org, click "New Project", and give it a name. The three default environments are created for you.
mellon setup saves the default project and environment for the current directory. This is a local-only setting stored in ~/.mellon/config.json, not in the repository. After setup, every mellon run in that directory (or any subdirectory) resolves the right secrets without extra flags.
Run it in the project root for single-project repos, or in each subfolder of a monorepo:
# single project cd my-app mellon setup --project <PROJECT_ID> --env dev # monorepo cd monorepo/api && mellon setup --project api_xxx --env dev cd monorepo/web && mellon setup --project web_xxx --env dev
Since the config lives on your machine (not in the repo), you need to re-run mellon setup after cloning on a new machine. If you prefer not to run setup at all, you can always pass --project and --env explicitly:
mellon run --project <PROJECT_ID> -c dev -- next dev
Without flags, mellon setup shows an interactive picker. Use --project and --env for non-interactive/CI workflows.

Add secrets

Add the secrets your app needs. You can set real values now, or leave them empty and fill them in later from the dashboard.
mellon secrets set DATABASE_URL "postgres://localhost:5432/mydb" -c dev mellon secrets set API_KEY "" -c dev mellon secrets set AUTH_SECRET "" -c dev
Repeat for other environments:
mellon secrets set DATABASE_URL "" -c preview mellon secrets set DATABASE_URL "" -c prod
For encryption keys or auth secrets, generate a real random value right away:
mellon secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c dev mellon secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c preview mellon secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c prod
Dashboard: Open your project at mellon.jsj.sh/orgs/<ORG_ID>/projects/<PROJECT_ID>/envs/dev to add or edit secrets from the web UI. You can toggle between environments using the tabs.

Verify and run

mellon secrets -c dev # list secret names (values hidden) mellon run -c dev -- bun dev

Features

FeatureDescription
Secret injectionmellon run -- <cmd> injects secrets as env vars, no files on disk
Output redactionHigh-entropy values automatically replaced with * in stdout/stderr
File mount--mount .env writes secrets to the given file path, deletes it after the process exits
OrganizationsMulti-tenant orgs with admin/member roles and invite links
Projects & environmentsOrganize secrets into projects with dev/preview/production environments
Audit logAppend-only event log tracks every secret change with user attribution
API tokensScoped to project or single environment, SHA-256 hashed, shown once
Device flowRFC 8628 login for CLI and agents, no copy-pasting tokens
AES-256-GCM encryptionEvery secret encrypted at rest with a random 12-byte IV
Download formatsExport as json, env, yaml, docker, dotnet-json, xargs
Web UIFull management dashboard with Doppler-style hidden values
Self-hostableRuns on Cloudflare Workers + D1, deploy your own instance
REST APIOpenAPI-documented API for building custom integrations

CLI reference

mellon login

Authenticate via device flow or bearer token.
mellon login # interactive device flow mellon login --token mel_xxx # save existing API token mellon login --api-url https://my-instance.dev --scope . # custom instance, scoped to current dir

mellon setup

Save the default project and environment for the current directory. This is stored locally in ~/.mellon/config.json, not in the repo, so it needs to be done on each machine after cloning. Run it in the project root, or in each subfolder of a monorepo. You can skip setup entirely by always passing --project and --env flags to other commands.
mellon setup # interactive project/env picker mellon setup --project proj_abc --env dev # non-interactive

mellon run

Execute a command with secrets injected as environment variables.
mellon run -- next dev # inject secrets from the configured env mellon run -c dev -- next dev # use the dev environment mellon run -c preview -- next dev # use the preview environment mellon run -c production -- next build # use the production environment mellon run -- printenv # verify which vars are injected (values redacted) mellon run --command 'echo $MY_SECRET' # shell string mode mellon run --mount .env -- npm start # write to file, clean up after mellon run --mount config.json --mount-format json -- next dev # mount as JSON mellon run --disable-redaction -- ./my-script.sh # opt out of output redaction
Use --command when you need shell features like &&, pipes, redirects, or $VARIABLE expansion. Wrap the command in single quotes so your parent shell does not expand secret variables before Mellon injects them.
# Wrong: your shell expands $DATABASE_URL before mellon starts mellon run --command "psql $DATABASE_URL -c 'select 1'" # Right: $DATABASE_URL expands inside mellon's child shell mellon run --command 'psql $DATABASE_URL -c "select 1"'
Put non-secret env vars before mellon run, especially in package scripts. This keeps regular build flags visible while secrets still come from Mellon.
{ "scripts": { "deployment": "CLOUDFLARE_ENV=preview mellon run -c preview --command 'vite build && wrangler deploy --env preview'" } }
Output redaction is enabled by default. Secret values with high entropy (>=3.5 Shannon bits, >=16 chars) are replaced with * in stdout/stderr. This prevents secrets from leaking into agent context windows or CI logs.

Local package binaries

When you run mellon run through a package manager script (bun run or npm run), the package manager adds node_modules/.bin to PATH before Mellon starts. Mellon inherits that PATH and passes it to the child process, so local binaries like vite, tsc, and wrangler are available without another launcher.
# in package.json scripts, local bins just work: mellon run -- vite build # vite found via node_modules/.bin mellon run -- wrangler deploy # wrangler found via node_modules/.bin mellon run -- tsc --noEmit # tsc found via node_modules/.bin # same with --command: mellon run --command 'vite build && wrangler deploy'
This also works when running mellon run directly with bunx:
bunx mellon run -- vite dev bunx mellon run -- next build
If you installed Mellon globally (via curl or npm i -g), running mellon run outside a package manager script means node_modules/.bin is not in PATH. In that case, use the full path or prefix with npx/bunx inside the child command, or run Mellon from a package script instead.

mellon secrets

Manage individual secrets.
mellon secrets # list secret names mellon secrets get DATABASE_URL # get a single value mellon secrets get DATABASE_URL --force # allow value output inside agent shells mellon secrets set API_KEY sk-live-xxx # set a value echo "multiline\nvalue" | mellon secrets set CERT # set from stdin mellon secrets delete OLD_KEY # delete mellon secrets download # download all (YAML) mellon secrets download --format json # download as JSON mellon secrets download --format env # download as .env
Inside AI agent shells, secrets get and secrets download refuse to print raw values to a terminal unless you pass --force. Prefer mellon run or a direct pipe so secret values go straight to the tool that needs them, not into the chat context.
mellon run --command 'psql "$DATABASE_URL" -c "select 1"' mellon secrets download --format env | fly secrets import --app my-app

mellon projects

mellon projects # list all projects mellon projects create --org org_abc --name my-app # create project mellon projects get proj_abc # show project details mellon projects update proj_abc --name new-name # rename mellon projects delete proj_abc # delete

mellon environments

mellon environments # list environments mellon environments create --project proj_abc --name Staging --slug staging # create mellon environments rename env_abc --name Production --slug prod # rename mellon environments delete env_abc # delete

Global flags

Most commands that resolve auth, project, or environment from config accept these overrides:
FlagEnv varDescription
--token <mel_xxx>MELLON_TOKENBearer token for auth
--api-url <url>MELLON_API_URLAPI endpoint (default: https://mellon.jsj.sh)
--env <slug> / --config <slug> / -c <slug>MELLON_ENVIRONMENTEnvironment slug (e.g. dev, prod)
--project <id> / -p <id>MELLON_PROJECTProject ID override

Download formats

FormatFlagUse case
json--format jsonApplication config files
env--format envShell scripts with quotes
env-no-quotes--format env-no-quotesShell scripts without quotes
yaml--format yamlDefault CLI output
docker--format dockerDocker --env-file
dotnet-json--format dotnet-json.NET appsettings.json (uses __ for nested keys)
xargs--format xargsNUL-delimited pairs for shell pipelines

Integrations

Cloudflare Workers

Upload secrets to a Cloudflare Worker using wrangler secret bulk:
mellon secrets download -c production --format env | wrangler secret bulk --env=""
The pipe sends the complete Mellon environment directly to Wrangler. Secret values never appear in the terminal and no .env file remains on disk. Use an explicit empty environment for the top-level production Worker; Wrangler warns when a configuration has named environments but the target is ambiguous.
Add these as package.json scripts so you can sync before each deploy:
{ "scripts": { "secrets:preview": "mellon secrets download -c preview --format env | wrangler secret bulk --env preview", "secrets:production": "mellon secrets download -c production --format env | wrangler secret bulk --env=\"\"" } }
This intentionally syncs the entire selected environment. Keep dev, preview, and production values separate in Mellon, then use the matching Wrangler environment at deployment time.

Vercel

vercel env add only accepts one variable at a time. Use the xargs format to pipe them:
mellon secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --force' sh
Add --sensitive to mark values as sensitive in Vercel:
mellon secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --sensitive --force' sh
As a package.json script:
{ "scripts": { "secrets:vercel": "mellon secrets download -c production --format xargs | xargs -0 -n2 sh -c 'printf %s \"$2\" | vercel env add \"$1\" production --sensitive --force' sh" } }

Fly.io

fly secrets import reads NAME=VALUE pairs from stdin. Pipe mellon secrets download directly, no temp file needed:
mellon secrets download -c production --format env | fly secrets import --app my-app
By default fly secrets import triggers a machine restart once secrets are staged. Use --stage to skip the restart and deploy separately:
# stage without restarting mellon secrets download -c production --format env | fly secrets import --app my-app --stage # then deploy when ready fly deploy --app my-app
Add as package.json scripts:
{ "scripts": { "secrets:fly:production": "mellon secrets download -c production --format env | fly secrets import --app my-app", "secrets:fly:preview": "mellon secrets download -c preview --format env | fly secrets import --app my-app-staging" } }

Docker

Mount secrets as a Docker env file:
mellon secrets download --format docker > .env.docker docker run --env-file .env.docker my-image
Or inject at build time:
mellon run -- docker compose up

CI / GitHub Actions

Use an API token for non-interactive environments:
- name: Run with secrets env: MELLON_TOKEN: ${{ secrets.MELLON_TOKEN }} MELLON_PROJECT: ${{ vars.MELLON_PROJECT }} MELLON_ENVIRONMENT: ${{ vars.MELLON_ENVIRONMENT }} run: | npx mellon run -- next build

.NET

Download secrets as a hierarchical JSON file (keys with __ become nested objects):
mellon secrets download --format dotnet-json > appsettings.Secrets.json
DB__HOST=localhost becomes { "Db": { "Host": "localhost" } }.

Self-hosting

Mellon runs on Cloudflare Workers + D1. You only need to deploy the App Worker. The Provider Worker (authentication) is optional because your self-hosted instance can use the hosted provider at auth.mellon.jsj.sh by default.
Your Cloudflare account Mellon Cloud ┌──────────────────────┐ ┌──────────────────────┐ │ App Worker │ OAuth │ Provider Worker │ │ (your secrets) │────────────▶│ (auth.mellon.jsj.sh) │ │ │ PKCE │ │ │ You deploy this │◀────────────│ Already running │ └──────────────────────┘ └──────────────────────┘
This means you don't need Google OAuth credentials and the deployment is a single worker.

One command deploy

The fastest way to self-host — no git clone, no build step:
npx mellon self-host
It logs into Cloudflare (reusing your wrangler login when present, or an OAuth browser flow, or a pre-filled API token link that works over SSH), creates the Worker and D1 database, applies migrations, and prints your instance URL. Re-run the same command anytime to update — only new migrations are applied and your auth secret is never rotated.
# non-interactive (CI/agents) CLOUDFLARE_API_TOKEN=xxx npx mellon self-host --yes # custom worker name and domain npx mellon self-host --name mellon --domain secrets.acme.com

Deploy from source

  1. Clone the repo and install dependencies:
git clone https://github.com/jsj/mellon.git cd mellon && bun install
  1. Create app/.dev.vars with your secrets:
BETTER_AUTH_SECRET=<any random string> ENCRYPTION_KEY=<output of: openssl rand -base64 32>
  1. Run locally:
bun --cwd app run dev
  1. Deploy:
bun --cwd app run deployment # deploy preview worker bun --cwd app run deployment:prod # deploy production worker
The app auto-registers with auth.mellon.jsj.sh on first request via RFC 7591 dynamic client registration. No Google OAuth credentials needed, no manual setup.

Self-hosting the provider (optional)

By default, your self-hosted app uses auth.mellon.jsj.sh for authentication. If you want a fully air-gapped setup with no dependency on Mellon cloud, you can deploy the Provider Worker yourself.
  1. Create provider/.dev.vars (requires Google OAuth credentials):
BETTER_AUTH_SECRET=<any random string> GOOGLE_CLIENT_ID=<your Google OAuth client ID> GOOGLE_CLIENT_SECRET=<your Google OAuth client secret>
  1. Deploy the provider:
bun --cwd provider run deployment # deploy preview bun --cwd provider run deployment:prod # deploy production
  1. Point the app at your self-hosted provider by changing PROVIDER_URL in app/wrangler.jsonc:
{ "vars": { "PROVIDER_URL": "https://your-provider.your-domain.com" } }
Then redeploy the app. It will auto-register with your provider on the next request.

How it works

Architecture
Mellon is two Cloudflare Workers in a monorepo, each backed by a D1 (SQLite) database:
┌─────────────────────────────────────────────────────────────────┐ │ Your Machine │ │ │ │ mellon run -- next dev │ │ │ │ │ │ device flow login (RFC 8628) │ │ │ or bearer token │ │ ▼ │ │ ┌──────────┐ │ │ │ Mellon │ │ │ │ CLI │ │ │ └────┬─────┘ │ │ │ │ └───────┼─────────────────────────────────────────────────────────┘ │ REST API ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ App Worker │ │ Provider Worker │ │ (self-hosted) │────────▶│ (auth.mellon.jsj.sh) │ │ │ OAuth │ │ │ • Secrets CRUD │ PKCE │ • Google login │ │ • AES-256-GCM │ │ • OAuth2 / OIDC │ │ • Audit log │◀────────│ • Dynamic client │ │ • API tokens │ token │ registration │ │ • Device flow │ │ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ │ D1 (app) │ │ │ │ D1 (auth) │ │ │ └────────────┘ │ │ └────────────┘ │ └──────────────────────┘ └──────────────────────┘
App: the secret manager you self-host. Handles secrets encryption, organizations, projects, environments, and the web UI.
Provider: centralized OAuth provider at auth.mellon.jsj.sh. Self-hosted instances register automatically via RFC 7591 dynamic client registration as public PKCE clients (no client secret needed).
Auth flow
CLI/Agent App (self-hosted) Provider (auth.mellon.jsj.sh) │ │ │ │ POST /api/auth/device/code │ │ │─────────────────────────────▶│ │ │ { user_code, device_code } │ │ │◀─────────────────────────────│ │ │ │ │ │ User opens /device │ │ │ and enters user_code │ │ │ ┌────────────────────┼────── redirect ───────────────▶│ │ │ │ │ │ │ │ Google sign-in ──▶│ Google │ │ │ ◀── callback ─────│ │ │ │ │ │ │ │◀── auth code (PKCE) ───────────│ │ └────────────────────┼────── approved ───────────────▶│ │ │ │ │ Poll /api/auth/device/token │ │ │─────────────────────────────▶│ │ │ { access_token } │ │ │◀─────────────────────────────│ │
Local dev vs CI authentication
Two auth paths depending on the environment:
Local development CI / GitHub Actions ───────────────── ─────────────────── mellon login MELLON_TOKEN=mel_xxx │ │ ▼ │ Browser opens /device │ │ │ ▼ │ Enter user_code │ │ │ ▼ │ Google sign-in │ │ │ ▼ ▼ Session cookie saved Bearer token from env in ~/.mellon/config.json var or GitHub secret │ │ ▼ ▼ mellon run -- next dev mellon run -- next build
Local: interactive device flow (RFC 8628). Run mellon login once, then the session is reused.
CI: set MELLON_TOKEN as a secret in your CI provider. No browser needed, no interactive prompts.
Secrets encryption
Every secret value is AES-256-GCM encrypted before storage. Each write generates a random 12-byte IV. The encryption key is either:
  • ENCRYPTION_KEY: 32 random bytes, base64-encoded (openssl rand -base64 32)
  • Derived from BETTER_AUTH_SECRET via SHA-256 (default if ENCRYPTION_KEY is not set)
For rotation, use ENCRYPTION_KEYS with named versions and select the active version with ENCRYPTION_KEY_VERSION. Each ciphertext records its key version. Keep old keys configured while mellon keys rotate --to <version> --apply re-encrypts and verifies bounded batches. Remove an old key only after rotation, normal reads, backups, and restore validation all pass.
plaintext value ("sk-live-xxx") │ ▼ ┌─────────────┐ ┌──────────────┐ │ AES-256-GCM │◀────│ 12-byte │ │ encrypt │ │ random IV │ └──────┬──────┘ └──────────────┘ │ ▼ ┌────────────────────────────────┐ │ secretEvent (append-only row) │ │ │ │ action: "set" │ │ name: "API_KEY" │ │ value: <iv>:<ciphertext> │ │ userId: usr_abc │ │ createdAt: 1719000000 │ └────────────────────────────────┘
Secrets are stored as an append-only event log. Current values are derived by replaying events. This gives you a full audit trail of every change with user/token attribution.
REST API
The app exposes a full REST API with OpenAPI documentation at /api/openapi.json.
# list secrets curl -H "Authorization: Bearer mel_xxx" \ https://mellon.jsj.sh/api/environments/{envId}/secrets # set a secret curl -X POST -H "Authorization: Bearer mel_xxx" \ -H "Content-Type: application/json" \ -d '{"name": "API_KEY", "value": "sk-live-xxx"}' \ https://mellon.jsj.sh/api/environments/{envId}/secrets # bulk download as JSON curl -H "Authorization: Bearer mel_xxx" \ https://mellon.jsj.sh/api/environments/{envId}/secrets/download?format=json # bulk set curl -X PUT -H "Authorization: Bearer mel_xxx" \ -H "Content-Type: application/json" \ -d '{"secrets": {"KEY1": "val1", "KEY2": "val2"}}' \ https://mellon.jsj.sh/api/environments/{envId}/secrets

License

MIT