In order to make deployments to Vercel fully repeatable and automatable I wanted the entire process to be encapsulated in the build process and happen using the CLI, rather than requiring some of the work having to be done manually through the Vercel UI, e.g. setting up projects and adding sensible defaults for environment variables etc.). Some of the steps that I wanted to be able to automate are:
  1. Create the "Project" in Vercel
  2. Deploy an application (from within a monorepo)
  3. Configure the project as part of the deployment, to avoid having to configure it through the UI
  4. Configure the deployed application environment variables with sensible defaults from .env
  5. Layer on environment specific environment variables (similar to how Helm ".values" files work)
To accomplish this, I created a makefile which can be used in conjunction with some config files, to perform the transformations and CLI operations to automate the deployment. The initial folder structure looks like this: / <-- monorepo root /apps/exampleApp/* <-- Next.js application /apps/exampleApp/.env <-- Next.js application default variables /build/deploy/makefile <-- deployment commands /build/deploy/dev.env.json <-- development environment specific variables /build/deploy/vercel.json <-- Vercel project configurations /build/deploy/token <-- Vercel CLI token, this should be swapped in from secrets /packages/* <-- other npm projects in the monorepo The makefile contents looks as follows:

.PHONY: create
.PHONY: deploy-dev

MAKEFILE_PATH:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
ROOT_PATH:=$(shell realpath "${MAKEFILE_PATH}/../..")

create:
	npx vercel project add YOUR_PROJECT_NAME --token "$(shell cat token)"

../../.vercel/project.json: 
	npx vercel link --cwd ${ROOT_PATH} -p YOUR_PROJECT_NAME --yes --token "$(shell cat token)"

../../.vercel/base-env.json: ../../apps/exampleApp/.env
	cat ../../apps/exampleApp/.env | \
	jq -Rn '{ "env": [inputs | select(length > 0 and (startswith("#") | not)) | capture("(?<key>.+?)=(?<value>.+)") | {(.key): .value }] | add }' \
	> ../../.vercel/base-env.json

../../.vercel/dev-local-config.json: vercel.json dev.env.json ../../.vercel/base-env.json
	jq -s '.[0] * .[1] * .[2]' vercel.json ../../.vercel/base-env.json dev.env.json \
	> ../../.vercel/dev-local-config.json

deploy-dev: ../../.vercel/project.json ../../.vercel/dev-local-config.json
	npx vercel deploy --cwd ${ROOT_PATH} --token "$(shell cat token)" --local-config ../../.vercel/dev-local-config.json

clean:
	rm -rf ../../.vercel
Essentially what you have is one command "create" to create the remote project in Vercel and one command "deploy-dev" to deploy the application using the development variables. All the other files are used to generate a custom configuration for the deploy step. The other significant files are: vercel.json - this is where you can configure the project settings of Vercel.

{
	"framework": "nextjs",
	"outputDirectory": "apps/exampleApp/.next",
	"env": {
		"EXAMPLE_SETTING": "some_value"
	}
}
dev.env.json - just the environment section for "dev" deployments, e.g.

{
    "env": {
        "EXAMPLE_SETTING_A": "dev.specific.value",
    }
}
The contents of your typical .env file might look like this:
EXAMPLE_SETTING_A="default.value"
EXAMPLE_SETTING_B="another one"
You will notice that the makefile also makes reference to several files in the .vercel folder, this folder is transient and is created by "vercel link" -- it isn't checked in to Git, but here's a description of what the files do: /.vercel <-- Created by "Vercel Link", this is not committed in Git /.vercel/project.json <-- Created by "Vercel Link" /.vercel/base-env.json <-- sensible defaults, created from .env by the makefile which replicate whatever is in .env for the app /.vercel/dev-local-config.json <-- the combined configuration values created by the makefile for the project + dev variables to be used on the CLI In the above example, the base-env.json would look like this:

{
    "env": {
        "EXAMPLE_SETTING_A": "default.value",
        "EXAMPLE_SETTING_B": "another one",
    }
}
The dev-local-config.json would look like:

{
	"framework": "nextjs",
	"outputDirectory": "apps/exampleApp/.next",
	"env": {
		"EXAMPLE_SETTING": "some_value",
                "EXAMPLE_SETTING_A": "dev.specific.value",
                "EXAMPLE_SETTING_B": "another one"
	}
}
So you can see, that the final configuration sent to Vercel for the "deploy-dev" step configures the project as Next.js, configures the location of the build asset and has a 3 way combined "env" section from "vercel.json" + ".env" + "dev.env.json" With this starting point you could now add more environments simply by having additional "*.env.json" files and replicating the makefile step to generate and use that config.