.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.12345# 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)
.env files: secrets live in the cloud and are easy to share across machines. No more "can you send me the .env?" on Slack..env files or pasting keys in DMs.-c production..env files, use mellon run to inject secrets into processes without exposing them.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.1npx -y skills add jsj/mellon
~/.mellon/bin):1curl -fsSL https://mellon.jsj.sh/install.sh | bash
1npm i -g mellon
12npx mellon run -- next dev bunx mellon run -- next dev
1mellon login
1mellon setup
~/.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.1mellon discover --path ~/Developer
1mellon sync cloudflare --project <project> --env <environment> --worker <worker-name>
--apply after you review the plan. Production environments also require --confirm-production.1mellon run -- next dev
.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.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
1mellon orgs create --name my-company
12mellon orgs # find your org ID mellon projects create --org <ORG_ID> --name my-app
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.1234567# 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
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:1mellon run --project <PROJECT_ID> -c dev -- next dev
mellon setup shows an interactive picker. Use --project and --env for non-interactive/CI workflows.123mellon secrets set DATABASE_URL "postgres://localhost:5432/mydb" -c dev mellon secrets set API_KEY "" -c dev mellon secrets set AUTH_SECRET "" -c dev
12mellon secrets set DATABASE_URL "" -c preview mellon secrets set DATABASE_URL "" -c prod
123mellon 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
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.12mellon secrets -c dev # list secret names (values hidden) mellon run -c dev -- bun dev
| Feature | Description |
| Secret injection | mellon run -- <cmd> injects secrets as env vars, no files on disk |
| Output redaction | High-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 |
| Organizations | Multi-tenant orgs with admin/member roles and invite links |
| Projects & environments | Organize secrets into projects with dev/preview/production environments |
| Audit log | Append-only event log tracks every secret change with user attribution |
| API tokens | Scoped to project or single environment, SHA-256 hashed, shown once |
| Device flow | RFC 8628 login for CLI and agents, no copy-pasting tokens |
| AES-256-GCM encryption | Every secret encrypted at rest with a random 12-byte IV |
| Download formats | Export as json, env, yaml, docker, dotnet-json, xargs |
| Web UI | Full management dashboard with Doppler-style hidden values |
| Self-hostable | Runs on Cloudflare Workers + D1, deploy your own instance |
| REST API | OpenAPI-documented API for building custom integrations |
mellon login123mellon 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~/.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.12mellon setup # interactive project/env picker mellon setup --project proj_abc --env dev # non-interactive
mellon run123456789mellon 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
--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.12345# 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"'
mellon run, especially in package scripts. This keeps regular build flags visible while secrets still come from Mellon.12345{ "scripts": { "deployment": "CLOUDFLARE_ENV=preview mellon run -c preview --command 'vite build && wrangler deploy --env preview'" } }
* in stdout/stderr. This prevents secrets from leaking into agent context windows or CI logs.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.1234567# 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'
mellon run directly with bunx:12bunx mellon run -- vite dev bunx mellon run -- next build
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 secrets123456789mellon 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
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.12mellon run --command 'psql "$DATABASE_URL" -c "select 1"' mellon secrets download --format env | fly secrets import --app my-app
mellon projects12345mellon 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 environments1234mellon 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
| Flag | Env var | Description |
--token <mel_xxx> | MELLON_TOKEN | Bearer token for auth |
--api-url <url> | MELLON_API_URL | API endpoint (default: https://mellon.jsj.sh) |
--env <slug> / --config <slug> / -c <slug> | MELLON_ENVIRONMENT | Environment slug (e.g. dev, prod) |
--project <id> / -p <id> | MELLON_PROJECT | Project ID override |
| Format | Flag | Use case |
json | --format json | Application config files |
env | --format env | Shell scripts with quotes |
env-no-quotes | --format env-no-quotes | Shell scripts without quotes |
yaml | --format yaml | Default CLI output |
docker | --format docker | Docker --env-file |
dotnet-json | --format dotnet-json | .NET appsettings.json (uses __ for nested keys) |
xargs | --format xargs | NUL-delimited pairs for shell pipelines |
wrangler secret bulk:12mellon secrets download -c production --format env | wrangler secret bulk --env=""
.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.package.json scripts so you can sync before each deploy:123456{ "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=\"\"" } }
dev,
preview, and production values separate in Mellon, then use the matching
Wrangler environment at deployment time.vercel env add only accepts one variable at a time. Use the xargs format to pipe them:12mellon secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --force' sh
--sensitive to mark values as sensitive in Vercel:12mellon secrets download -c production --format xargs | \ xargs -0 -n2 sh -c 'printf %s "$2" | vercel env add "$1" production --sensitive --force' sh
package.json script:12345{ "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 secrets import reads NAME=VALUE pairs from stdin. Pipe mellon secrets download directly, no temp file needed:1mellon secrets download -c production --format env | fly secrets import --app my-app
fly secrets import triggers a machine restart once secrets are staged. Use --stage to skip the restart and deploy separately:1234# 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
package.json scripts:123456{ "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" } }
12mellon secrets download --format docker > .env.docker docker run --env-file .env.docker my-image
1mellon run -- docker compose up
1234567- 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
__ become nested objects):1mellon secrets download --format dotnet-json > appsettings.Secrets.json
DB__HOST=localhost becomes { "Db": { "Host": "localhost" } }.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 │ └──────────────────────┘ └──────────────────────┘
1npx mellon self-host
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.12345# 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
12git clone https://github.com/jsj/mellon.git cd mellon && bun install
app/.dev.vars with your secrets:12BETTER_AUTH_SECRET=<any random string> ENCRYPTION_KEY=<output of: openssl rand -base64 32>
1bun --cwd app run dev
12bun --cwd app run deployment # deploy preview worker bun --cwd app run deployment:prod # deploy production worker
auth.mellon.jsj.sh on first request via RFC 7591 dynamic client registration. No Google OAuth credentials needed, no manual setup.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.provider/.dev.vars (requires Google OAuth credentials):123BETTER_AUTH_SECRET=<any random string> GOOGLE_CLIENT_ID=<your Google OAuth client ID> GOOGLE_CLIENT_SECRET=<your Google OAuth client secret>
12bun --cwd provider run deployment # deploy preview bun --cwd provider run deployment:prod # deploy production
PROVIDER_URL in app/wrangler.jsonc:12345{ "vars": { "PROVIDER_URL": "https://your-provider.your-domain.com" } }
┌─────────────────────────────────────────────────────────────────┐ │ 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) │ │ │ └────────────┘ │ │ └────────────┘ │ └──────────────────────┘ └──────────────────────┘
auth.mellon.jsj.sh. Self-hosted instances register automatically via RFC 7591 dynamic client registration as public PKCE clients (no client secret needed).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 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
mellon login once, then the session is reused.MELLON_TOKEN as a secret in your CI provider. No browser needed, no interactive prompts.ENCRYPTION_KEY: 32 random bytes, base64-encoded (openssl rand -base64 32)BETTER_AUTH_SECRET via SHA-256 (default if ENCRYPTION_KEY is not set)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 │ └────────────────────────────────┘
/api/openapi.json.12345678910111213141516171819# 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