What is encrypted, what our server can still see, and where the guarantee stops. The last part is the part most pages leave out, so it is on this page too.
Your values are encrypted on your Mac, with age, before they leave it.
Encrypted to the public key of every current member. Our relay stores the result and cannot decrypt it: no private key, no decrypt-capable dependency, no admin override that could become one.
Generated on your Mac and kept in the macOS Keychain. Never written to a file by us, never transmitted.
We did not invent any cryptography. age, X25519 recipients only.
We are not going to call this unhackable, military-grade, or zero-risk. Everything below is the shape of what it actually does.
This is the obvious contradiction to point at, and it deserves a straight answer rather than a deflection. There is a real trade-off here. It is not the one the question implies.
“End-to-end encrypted” is a statement about our servers. It has never
meant that your values exist nowhere in plaintext. They obviously do. They
are plaintext in your .env file. They are plaintext in your
editor. They are plaintext in your Mac's memory the moment you click the eye
icon to look at one. Decrypting values on your machine is the entire purpose
of the app.
The guarantee is about who else can. Our relay cannot, and the Vercel integration does not change that, because our relay is not in the path.
https://api.vercel.com directly from your machine, over Vercel's own API.Because a Connect button would have to route your Vercel token through our server, and we are not willing to hold it.
Vercel's OAuth token exchange requires a client_secret. A
client_secret cannot live inside a desktop app that anybody can
download and unpack, so the exchange would have to happen on our
infrastructure. And a Vercel access token reads your environment variables
in plaintext.
Our relay must never hold a credential that reads the values it is built to be unable to read. Being unable to decrypt your ciphertext would mean nothing if we held a key that fetched the same secrets from somewhere else.
The consequence is a worse first run: you go to Vercel, create a token, and paste it. That is a deliberate trade of convenience for a claim we can actually make. We will revisit it if Vercel ships PKCE or a device-authorisation grant, neither of which exists today.
Once you push to Vercel, Vercel has your values, and Vercel can read them.
That is not a leak or a loophole. It is what pushing means: your builds need those values, so they have to arrive in a form Vercel can use. No amount of encryption on our side changes what a third party can do with a secret you deliberately sent them.
So the honest framing is this. MiniEnv's guarantee covers our infrastructure. It does not, and cannot, extend to a service you choose to hand values to. Anyone claiming otherwise about any integration is selling something.
What we can do is make it a decision rather than a background process:
If you never use the integration, none of this applies to you. Your values leave your machine only as ciphertext.
Import is the same path in reverse: your Mac reads from Vercel directly and encrypts on arrival. It has one asymmetry worth knowing before you rely on it.
A Vercel variable marked Sensitive can be written but never read back, by anyone, through their API. That is Vercel's design, not a permission we lack. Those variables come back as a name with no value. The app lists them as unreadable and asks you to paste those few by hand. It will not import them as empty strings that later overwrite something real, and it will not report “imported 24” when six of them arrived blank.
Which means import works least well for people who followed Vercel's own security advice. We would rather say that than quietly write empty values.
A full compromise (database dump, disk image, hostile operator) yields this and nothing more.
| Every variable's value | Encrypted, unreadable |
|---|---|
| Your private key | Never leaves your Keychain |
Variable names, e.g. STRIPE_SECRET_KEY | Visible |
| Email addresses and public keys | Visible |
| Project and team names, who belongs to what | Visible |
| When a value last changed, and how many exist | Visible |
Names being visible is a real trade-off, not an oversight: the app has to show you a list of your variables without decrypting them. The practical advice is short. Do not put a secret in a variable name.
Stated plainly, because a security model that oversells itself is worse than none.
MiniEnv protects secrets in transit and at rest on the server. The moment you
pull a value it exists in plaintext in a .env file on your disk.
We write that file mode 600, but it is readable by you and by
anything running as you. Malware on an endpoint reads the .env
and the Keychain. Nothing here changes that.
Removing someone is not retroactive, and cannot be. It re-encrypts the project so they cannot read future values. The copy already on their laptop keeps working, forever.
If a departing person had a secret, that secret is compromised. Rotate it at the source: the database, the provider, not just here. The app says this at the moment you remove someone, and no part of the product is allowed to soften it.
A stolen token gets an attacker your project's ciphertext and metadata. It does not get them a single plaintext value: that needs the private key, which stays in the Keychain and is never transmitted. Token theft is a real incident. It is not a disclosure of your values.
The relay learns who belongs to which project, how many variables exist, what they are named, and when they change.
Both re-encrypt the whole project, on your Mac, before the change is accepted. There is no window where a removed person can still read new values, and no background job that gets to it later. A membership change that cannot be re-encrypted is refused rather than half-applied.
Only someone who can already decrypt a project can produce its re-encryption. That is physics, not policy: you cannot re-encrypt what you cannot read.
Two different things get asked here, and only one of them is refused on principle.
Recovery we can perform on your behalf is the one we will not build. No escrow, no backup we hold, no admin override, and there is not going to be one. A support-friendly recovery mechanism is a decryption mechanism, and the moment it exists everything else on this page stops being a property and becomes a promise.
A copy of your own key, held by you, is a different question, and we should be straight about where we are on it. Your private key is an age identity: one short line of text. Letting you keep a copy in your own password manager would give us nothing, would never touch our relay, and would not weaken anything above by one bit. We do not offer it today. The key is generated straight into the macOS Keychain and there is no export anywhere in the app or the command-line tool. That is a decision we are revisiting, not a property we are defending, and this page will say so on the day it changes.
So, as things actually stand: if other people are on the project, you do not need a backup. Any current member can re-encrypt it for your new key, on their Mac. If you lose the key and nobody else is on the project, the data is gone and we cannot get it back.
Recording a new public key grants no access on its own. It records a key. Access resumes only when an existing member re-encrypts for it.
Team membership is a roster entry. Reading a project requires membership of that project, because that is what the ciphertext is encrypted to. Neither is a substitute for the other.
It is read-only and cannot become otherwise. A browser cannot reach your Keychain, so the page holds no private key: it cannot show you a value, and it cannot re-encrypt, which is what every membership change requires. It will never ask you to paste a private key, and there is no server-side re-encryption endpoint behind it.
These are real, known, and written down rather than discovered. They are the reasons this is a small-team tool today.
node. Any process running as you can read
it.The claim that our relay cannot decrypt is enforced by a check that runs on every test run: it walks the relay's dependency tree and fails the build if anything decrypt-capable is linked in. It has been verified to actually catch a violation rather than merely existing.
The revocation guarantee is asserted end to end, in both directions: that a removed member cannot open the re-encrypted values, and that the copy they already downloaded still works. If only the first were tested, the test would pass on a product that quietly lied about the second.
# crypto invariants and revocation cd core && cargo test --features conformance # the relay's API, plus the dependency audit cd relay && npm test # the Rust and Node implementations agree on the wire format cd cli && npm test # two real users, a real revocation, end to end bash scripts/e2e.sh
Two independent implementations of one documented protocol, one in Rust and one in Node, sharing no code. They are tested against each other, which is how a wire format stays honest.
Write to support@minienv.dev. If it is a vulnerability, say so in the subject line and we will come back to you before doing anything else.