How to work around Vercel's 4kb environment restriction
Published: 2022-06-24 12:16:03

At Layer3 we run our whole platform in a full-stack beautifully orchestrated environment on Vercel. All server-side and client-side code is written in TypeScript and share many of the modules and types.

Everything was going well, until one day…

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/baf950equ6iom00yhpys.png) 

Vercel has a 4kb environment variable limit. It is caused by the underlying AWS Lambda infrastructure, but whereas AWS has some solutions for proper secret management, Vercel basically says you need to [roll your own](https://vercel.com/support/articles/how-do-i-workaround-vercel-s-4-kb-environment-variables-limit).

At this time, there are so many pros of being on a platform like Vercel, and we save so much time by not having to set up complex cloud infrastructure on AWS.

So we decided to fix this.

Our solution focused on two things:

- Secure management and deployment of secrets in preview and production
- Keep the excellent dev experience of pulling development keys to local environments

Check out the example repository here: [larskarbo/next-env-encrypt-decrypt](https://github.com/larskarbo/next-env-encrypt-decrypt/blob/main/README.md)

## Meet Doppler — an environment manager

Doppler is a service that specializes on environment variable management. It sounded perfect for our usecase, they even have a Vercel integration!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b228g0ttalei9153m7sd.png)
 

However, we quickly realized that even though Doppler has a Vercel integration, it doesn’t help the 4kb problem at all. It actually just — kind of *contributes* to it... By adding more `DOPPLER_` variables.

However, the Doppler interface and API is amazing, and we figured it we could make a working solution with some hacking.

## Fetching secrets from Doppler instead of Vercel

Once you add all your environment variables on Doppler instead of Vercel, you can work around the 4kb restriction pretty easily by fetching secrets from Doppler instead of Vercel.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eql4i1v99mjft4qznlxz.png)
 

The only environment variable you need on Vercel is a *Doppler token*. The easiest way to add tokens for `development`, `preview` and `production` is to have both Vercel CLI and Doppler CLI installed and generate three different keys from the terminal:

```bash
echo -n "$(doppler configs tokens create vercel-gitops --config dev --plain)" | vercel env add DOPPLER_TOKEN development

echo -n "$(doppler configs tokens create vercel-gitops --config stg --plain)" | vercel env add DOPPLER_TOKEN preview

echo -n "$(doppler configs tokens create vercel-gitops --config prd --plain)" | vercel env add DOPPLER_TOKEN production
```

We’ll then make a script `fetchSecrets.ts` that fetches these variables at build time and writes them to `.env`.

```tsx
import fs from "fs/promises";
import secrets from "@larskarbo/gitops-secrets";

async function main() {
  const payload = await secrets.providers.doppler.fetch();

  let envFile = "";

  Object.entries({
    ...payload,
  }).forEach(([key, value]) => {
    envFile += `${key}=${value}\n`;
  });

  envFile += `DOPPLER_TOKEN=${process.env.DOPPLER_TOKEN}\n`;

  await fs.writeFile(".env", envFile);
}

void main();
```

Changes in package.json:

```json
"scripts": {
	...
	"build": "npm run fetch-secrets && nextjs build",
	"fetch-secrets": "ts-node fetchSecrets.ts"
}
```

Yes, that's all you need.

In development, you’ll simply run `npm run fetch-env`. This flow doesn’t add a lot of moving parts and it feels very similar to the `vercel env pull` workflow.

## Taking it a step further with encrypted secrets

Now that we are rolling our own secrets management, why not take it a step further and improve the security?

The current environment variable setup [can be a security risk](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/). A rogue npm package could [dump all the freely available](https://youtu.be/jUhiPj6h_L8?t=795) `process.env` variables and send it to a remote server. And remember, this could be the dependency of one of your dependencies too. Most npm apps have a boatload of dependencies when you look at the dependency tree, so the surface risk area might be bigger than you think.

Our goal will be to create a system where:

- Secrets are always encrypted, both in transit and at rest.
- Secrets are difficult to unintentionally leak when consumed by the final application.

Many platforms have sophisticated solutions for this, like [AWS KMS](https://aws.amazon.com/kms/) and [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/). The idea is that these tools hold the secret in encrypted form and provides it to the application at runtime. 

We’ll solve this in a simple and custom way, with some unique considerations:

- We need `NEXT_PUBLIC_` variables to be available in the environment.
- We want to be able to override secrets with `.env.local` for our local dev environments.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rkl8kuksd0r80m4bdira.png)
 

Building on the Doppler setup, we'll add another environment variable to Vercel, `SECRETS_KEY`.

```bash
gen_key () { openssl rand -base64 32 }

gen_key | vercel env add SECRETS_KEY development
gen_key | vercel env add SECRETS_KEY preview
gen_key | vercel env add SECRETS_KEY production
```

Now we’ll make some changes to our `fetch-secrets.ts` script.

It needs to:

1. Fetch secrets from doppler.
2. Write all `NEXT_PUBLIC_` vars to `.env`
3. Write all other secrets to a special file `.encrypted-secrets`

Commit this file into git like this, and *then* add it to `.gitignore`. This allows us to run the app without depending on the generated file.

Our super-charged `fetch-secrets.ts` looks like this:

```tsx
import Cryptr from "cryptr";
import fs from "fs/promises";

import gitopsSecrets from "@larskarbo/gitops-secrets";
import { ENCRYPTED_SECRETS_FILE } from "../src/utils";

async function main() {
  const payload = await gitopsSecrets.providers.doppler.fetch();

  if (!process.env.SECRETS_KEY) {
    throw new Error("SECRETS_KEY is not set");
  }

  const cryptr = new Cryptr(process.env.SECRETS_KEY);

  const encryptedText = cryptr.encrypt(JSON.stringify(payload));

  await fs.writeFile(ENCRYPTED_SECRETS_FILE, encryptedText);

  let envFile = "";

  Object.entries({
    ...payload,
  })
    .filter(([key]) => key.startsWith("NEXT_PUBLIC_"))
    .forEach(([key, value]) => {
      envFile += `${key}=${value}\n`;
    });

  envFile += `DOPPLER_TOKEN=${process.env.DOPPLER_TOKEN}\n`;
  envFile += `SECRETS_KEY=${process.env.SECRETS_KEY}\n`;

  await fs.writeFile(".env", envFile);
}

void main();
```

Then we need to decrypt the secrets in the runtime code. We'll make a helper function for this.

```tsx
let decryptedSecrets: null | {
  [key: string]: string;
} = null;

import { readFileSync } from "fs";

import Cryptr from "cryptr";
import path from "path";

export const ENCRYPTED_SECRETS_FILE = ".encrypted-secrets";

export const getSecret = (key: string) => {
  // in case you have some overrides in `.env.local`
  if (process.env.NODE_ENV === "development" && process.env[key]) {
    return process.env[key];
  }

  // only decrypt secrets the first time
  if (!decryptedSecrets) {
    if (!process.env.SECRETS_KEY) {
      return undefined;
    }

    const encryptedSecrets = readFileSync(
      path.join(process.cwd(), ENCRYPTED_SECRETS_FILE),
      "utf8"
    );
    const cryptr = new Cryptr(process.env.SECRETS_KEY);
    decryptedSecrets = JSON.parse(cryptr.decrypt(encryptedSecrets));
  }

  return decryptedSecrets?.[key];
};
```

Voila! Now you can use the secrets anywhere in your app like this:

```tsx
// back-end
const apiKey = getSecret("API_KEY")

// front-end
const somePublicKey = process.env.NEXT_PUBLIC_KEY
```

Check out the working demo here: ([link](https://next-env-encrypt-decrypt.vercel.app/), [github repo](https://github.com/larskarbo/next-env-encrypt-decrypt)).

## Conclusion

Vercel may have a 4kb environment restriction, but with some creative engineering, you might end up with a system that is more developer-friendly and secure than before.

This approach might be right if you are an early stage startup. When you get bigger, and have stricter requirements to managing confidential stuff, you’ll probably end up with a more complex cloud orchestrated infrastructure.

At Layer3, we use Vercel and Doppler to move fast. If you enjoyed this post and love the idea of building new types of applications that take advantage of the decentralised web, you should [join our team](https://www.notion.so/Open-Roles-c7edc26d9c314721a058b2ffc7b711b9)!