# EnvShare: passing secrets by link without handing over the key

> EnvShare is a small Next.js application that shares environment variables through a link, encrypting the payload with AES-GCM in the browser so the key never leaves the browser and the server only ever holds ciphertext. It is easy to self-host on Vercel with Upstash Redis, and the API is deliberately small, which is both its strength and its limit.

**chronark/envshare** — Share Environnment Variables Securely

- Repository: https://github.com/chronark/envshare
- Website: https://envshare.dev
- Stars: 665 · Forks: 73
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/chronark-envshare

## The encryption happens before the request leaves the page

EnvShare is a simple tool to share environment variables securely, and the whole design rests on one sentence in the README: it uses AES-GCM to encrypt your data before sending it to the server, and the encryption key never leaves your browser.

That is the mechanism, and it is worth being precise about what it buys you. The server never sees the plaintext and never sees the key. What it stores is a blob of ciphertext plus the metadata needed to serve it back. The recipient gets the key out of band, through whatever channel you used to send the link, and the browser on their side uses it to decrypt.

The four documented features follow from that design. Shareable links let you send the environment variables by sending a link. End-to-end encryption uses AES-GCM before the data is sent. The number of reads can be limited, and links can be set to auto expire and delete the data after a certain time.

The read limit is the feature that makes this usable rather than merely interesting, because a link that can be opened once and then dies is a very different object from a URL pasted into a chat channel. The expiry is the second half of that, since a link that is read ten times but lives for thirty days is still a leak waiting to happen.

So the model is closer to a password reset link than to a shared vault, and that framing is how the API behaves.

## The API is two endpoints and a set of headers

The store endpoint is documented with an unusually emphatic warning. The documentation says PLEASE NEVER EVER UPLOAD UNENCRYPTED SECRETS, because this endpoint is only meant to store already encrypted secrets, and it adds that the encrypted secrets are stored in plain text.

That second clause is the one to dwell on. The server stores the ciphertext as plain text, which is the point of the design, but it also means the server is trusted not to be malicious and not to be compromised in a way that lets it hand the ciphertext to someone who also has the key. In practice the key is what protects the data, and the ciphertext on the server is useless without it.

Storing a secret is a POST with the encrypted payload in the body:

```bash
$ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret"
```

Two optional headers configure the lifetime and the number of reads:

```bash
$ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret" -H "envshare-ttl: 3600" -H "envshare-reads: 10"
```

The defaults are documented precisely. Omitting envshare-ttl sets a default of 30 days, and the ttl is disabled by setting it to 0. Omitting envshare-reads simply disables the read limit and allows reading an unlimited number of times.

So the safe configuration is not the default one, and it is worth noticing that. The default leaves a link readable forever, and only the explicit read header closes it.

## What the response gives you back

A successful store returns a JSON object with the id and the effective settings:

```json
{
  "data": {
    "id": "HdPbXgpvUvNk43oxSdK97u",
    "ttl": 86400,
    "reads": 2,
    "expiresAt": "2023-01-19T20:47:28.383Z",
    "url": "http://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u"
  }
}
```

The id is the handle you share. The ttl and reads fields report what was actually applied rather than what you asked for, which is a useful detail, and expiresAt gives the moment the data will be removed.

Retrieval is the mirror image, and it needs the id, which is returned when you store the secret:

```bash
$ curl -s https://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u
```

and the response reports what is left:

```json
{
  "data": {
    "secret": "Hello",
    "remainingReads": 1
  }
}
```

The remainingReads field is what the client uses to know whether to prompt the user, and it is the visible side of the read counter that decrements on each retrieval.

The interface is thin by design. There is no listing endpoint, no search and no update, so a link id is the only handle on a secret and losing it means losing access. There is also no authentication on the API, which follows from the design: possession of the id is the authorisation, and the read limit is the revocation.

## The stack is small enough to read in one sitting

The built-with list is short: Next.js, tailwindcss, deployed on Vercel, with data stored on Upstash. Everything else follows from that.

The package manifest confirms the versions and the shape of the application. The package is named envshare, marked private, at version 0.1.0, with scripts for next dev, next build, next start, next lint, a jest test run with coverage, and a fmt script that runs rome. The runtime dependencies are @upstash/redis for the store, @vercel/analytics, @vercel/og for social image generation, zod for validation, base-x for the base64 variant used on the wire, react and react-dom at 18, next at 13.1.1 and typescript at 4.9.4. The dev dependencies pull in jest with ts-jest, tailwindcss, postcss with autoprefixer, and rome as the formatter and linter.

The repository layout is consistent with a single Next.js application: an app/ directory, a pages/ directory, pkg/ for the server-side logic, util/ for helpers, a public/ directory for static assets, an img/ directory, and configuration for jest, next, postcss, tailwind, typescript and rome. There is a .vscode directory, an .eslintrc.json and a .dockerignore.

zod being in the runtime dependencies is the detail that matters for the API. The stored and retrieved payloads are untrusted input at the boundary, and validating them with a schema rather than trusting the shape is the difference between a URL that is read once and one that can be used to inject data into a reader's application.

## Deploying it yourself takes a Redis and a Vercel account

The deployment story is the simplest part of the project. Detailed instructions are at the deploy page, and the requirement is small: all you need is a Redis database on Upstash and a Vercel account, with a one-click clone and deploy button in the README for the project template.

The container path is more interesting, because the Dockerfile shows what a self-hosted deployment costs. The build is a multi-stage build on node:20-alpine, and pnpm is installed through corepack with a pinned version, pnpm@10.18.2, so that the lockfile drives the build rather than whatever pnpm happens to be installed. The builder stage copies the manifest and lockfile, runs pnpm install with the frozen lockfile flag, copies the source and runs pnpm build.

The runner stage is the part worth reading. It sets NODE_ENV to production, PORT to 8080 and HOSTNAME to 0.0.0.0, creates a system group and a system user both with the id 1001, copies only the public directory and the standalone and static Next.js output, switches to the unprivileged user, exposes 8080 and runs node server.js.

The standalone output is what makes that possible. Next.js can emit a self-contained server bundle, and the image copies .next/standalone, .next/static and public, and nothing else. There is no package manager in the runtime image and no node_modules to keep in sync, and the process runs as a named non-root user rather than as root. For a service whose job is to hold encrypted blobs for a limited time, that is a proportionate amount of container engineering.

## Where EnvShare is the wrong choice

The first limitation is that it is a link, not a store. There is no namespace, no listing, no rotation and no history. The id is the handle, and if you lose the id you lose the data. For a one-off handoff of a dozen environment variables to a colleague, that is exactly right. For managing configuration across a team, it is the wrong shape.

The second limitation is that the expiry and read limits are enforced server side, and a server that is a hosted third party is a server you do not control. The self-hosting option fixes that, and it is genuinely easy, but it also means you now run a service whose job is to hold secrets, even if they are encrypted. The Dockerfile shows how small that service is, and a small service is a small attack surface, but it is still yours.

The third limitation is that the model does not cover asymmetric sharing. The key never leaves your browser, so the key has to reach the recipient separately from the link, which means two channels and two things to send. There is no mechanism in the documented API for a third party to be granted scoped access, and no server-side identity, because there are no accounts.

The real alternative, and it is the right one for anything long-lived, is a proper secrets manager. AWS Secrets Manager, HashiCorp Vault, or a cloud key management service all give you durable storage, access control, audit trails, rotation and revocation, and they are the correct answer whenever a value has to be shared repeatedly or used by a running service. The difference in approach is that EnvShare moves a value from one person to another once, encrypted at the edge, and a secrets manager holds values that services need continuously, with policy attached. EnvShare's advantage over a secrets manager is precisely the absence of accounts and infrastructure. Where the two meet is the awkward middle, a configuration set that several people need to reach over a few days, and for that case a read limit and an expiry are a reasonable answer.

## Conclusion

Adopt EnvShare if you need to move a set of environment variables to a colleague or a CI job occasionally, and if the read-limit and expiry model matches how much exposure you can accept. Do not adopt it as a secret store, since the server holds ciphertext and your readers hold the key, and do not expect it to cover the case where the recipient needs a different set of values. Before you deploy your own instance, read the API section carefully, because the store endpoint accepts only already encrypted secrets and the documentation is emphatic about that, and decide your own ttl and read limits rather than taking the defaults.

## FAQ

### How does EnvShare encrypt the data it stores?

It uses AES-GCM to encrypt your data in the browser before sending it to the server, and the encryption key never leaves your browser. The server stores the encrypted secrets as plain text, so only a reader who also has the key can decrypt the payload.

### How do I store and retrieve a secret with the EnvShare API?

Store one by POSTing the already encrypted payload to the secret endpoint, optionally with the envshare-ttl and envshare-reads headers, and retrieve one by GETting the id that the store response returns. The API section warns explicitly never to upload unencrypted secrets, because the store endpoint accepts only already encrypted data.

### What are the default expiry and read limits for EnvShare links?

Omitting the envshare-ttl header sets a default of 30 days, and the ttl can be disabled by setting it to 0. Omitting the envshare-reads header disables the read limit and allows the link to be read an unlimited number of times, so the safe configuration has to be set explicitly.

### What do I need to self-host EnvShare?

All you need is a Redis database on Upstash and a Vercel account, and the README provides a one-click clone and deploy button for the project template. The repository also has a Dockerfile with a multi-stage build that produces a non-root Node.js 20 image running on port 8080 with pnpm installed through corepack.

### What is the configuration option in EnvShare?

There is one documented environment variable, ENABLE_VERCEL_ANALYTICS, where any truthy value enables Vercel Analytics and it is turned off by default. The contributing notes also say the repository uses pnpm, that it can be installed with npm install -g pnpm, and that pnpm fmt should be run before committing.

## Sources

- [chronark/envshare on GitHub](https://github.com/chronark/envshare)
- [Issues](https://github.com/chronark/envshare/issues)
- [License: MIT](https://github.com/chronark/envshare/blob/main/LICENSE)
- [Project website](https://envshare.dev)
- [README](https://github.com/chronark/envshare/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/chronark-envshare
