# Environment variables

> VERCEL_TOKEN, VERCEL_PROJECT_ID, VERCEL_TEAM_ID, and arbitrary ${VAR_NAME} values interpolated into string fields.

- Repository: jaredpalmer/vwaffle
- GitHub: https://github.com/jaredpalmer/vwaffle
- Human docs: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581
- Complete Markdown: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581/llms-full.txt

## Source Files

- `src/api.ts`
- `src/config.ts`
- `src/index.ts`
- `README.md`

---

---
title: "Environment variables"
description: "VERCEL_TOKEN, VERCEL_PROJECT_ID, VERCEL_TEAM_ID, and arbitrary ${VAR_NAME} values interpolated into string fields."
---

`resolveContext` reads `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, and `VERCEL_TEAM_ID` (plus optional `VERCEL_API_URL`) to authenticate Firewall API calls. `loadDesiredConfig` then expands any `${VAR_NAME}` placeholder in string fields of the desired JSON from `process.env`, records resolved values for redaction, and either fails `apply` or drops affected `rules` on `plan` / `apply --dry-run` when a name is unset.

## Context variables

These process-environment keys select the Vercel account, project, team, and API origin. `vwaffle help` documents the first three. `init`, `help`, and `version` do not call `resolveContext`. `apply --dry-run` also skips it.

| Variable | Required | Used by | Role |
| --- | --- | --- | --- |
| `VERCEL_TOKEN` | Yes for `pull`, `plan`, and `apply --yes` | `resolveContext` → `Authorization: Bearer …` | API token from [vercel.com/account/tokens](https://vercel.com/account/tokens) |
| `VERCEL_PROJECT_ID` | When `--project` and `.vercel/project.json` `projectId` are absent | `resolveContext` → `projectId` query param | Project to target |
| `VERCEL_TEAM_ID` | No | `resolveContext` → `teamId` query param when set | Team scope for the token |
| `VERCEL_API_URL` | No | `API_URL` in `src/api.ts` | API origin; default `https://api.vercel.com` |

<ParamField body="VERCEL_TOKEN" type="string" required>
Bearer token. `resolveContext` throws if `process.env.VERCEL_TOKEN` is missing. There is no CLI flag override.
</ParamField>

<ParamField body="VERCEL_PROJECT_ID" type="string">
Used only when `--project` is omitted. If this and `.vercel/project.json` `projectId` are also unset, `resolveContext` throws.
</ParamField>

<ParamField body="VERCEL_TEAM_ID" type="string">
Used only when `--team` is omitted. Falls back to `.vercel/project.json` `orgId`. Omitted from the request URL when no team ID is resolved.
</ParamField>

<ParamField body="VERCEL_API_URL" type="string">
Optional origin. The client always appends `/v1/security/firewall/config`. Not listed in `vwaffle help`.
</ParamField>

```sh
export VERCEL_TOKEN=...        # https://vercel.com/account/tokens
```

<Note>
There is no `.env` loader. Values must already be in the process environment (shell `export`, CI `env:`, or the parent process).
</Note>

## Resolution order

First match wins. Flags beat environment variables; environment variables beat `.vercel/project.json` from `vercel link`.

| Value | Sources |
| --- | --- |
| Token | `VERCEL_TOKEN` only |
| Project | `--project`, then `VERCEL_PROJECT_ID`, then `.vercel/project.json` `projectId` |
| Team | `--team`, then `VERCEL_TEAM_ID`, then `.vercel/project.json` `orgId` |

A missing `.vercel/project.json` is ignored when flags or environment variables supply the IDs. Linked-project JSON is read with `readJson` from `process.cwd()`.

`requestUrl` always sets `projectId`. It sets `teamId` only when `context.teamId` is truthy.

## Config interpolation

Any `${VAR_NAME}` inside a **string** field of the desired file (`firewall.config.json` by default, or `-c` / `--config`) is replaced from `process.env`. Object keys, numbers, booleans, and `null` are left unchanged. Nested objects and arrays are walked with JSONPath-style paths starting at `$`.

The placeholder grammar is `\$\{([A-Za-z_][A-Za-z0-9_]*)\}`:

- Matches `${INTERNAL_TOKEN}`, `${MY_SECRET}`, `${A1_b}`.
- Does not match `$VAR`, `${123}`, or `${name-with-dashes}`.

On a hit:

| `process.env[name]` | Result |
| --- | --- |
| Defined (including `""`) | Placeholder replaced; `secrets.set(name, replacement)` |
| `undefined` | Placeholder left in place; `missing` records `NAME (path)` |

Example path for a header bypass value: `$.rules[1].conditionGroup[0].conditions[0].value`.

```json
{
  "name": "Bypass for internal service",
  "active": true,
  "conditionGroup": [
    {
      "conditions": [
        {
          "type": "header",
          "op": "eq",
          "key": "x-internal-token",
          "value": "${INTERNAL_TOKEN}"
        }
      ]
    }
  ],
  "action": { "mitigate": { "action": "bypass" } }
}
```

<Tip>
Keep the placeholder in the versioned file. Set `INTERNAL_TOKEN` (or any other name) in the environment so the secret never lands in git.
</Tip>

`pull` writes the live API body as-is and does not interpolate. `init` writes a starter file with no placeholders.

## Missing variables by command

`loadDesiredConfig(path, { strict })` interpolates first, then branches on `missing`.

```mermaid
flowchart TD
  load["loadDesiredConfig(path, strict)"] --> interp["interpolate string fields from process.env"]
  interp --> miss{"missing.size > 0?"}
  miss -->|no| ok["return config and secrets"]
  miss -->|"yes and strict"| fail["throw missing environment variables"]
  miss -->|"yes and not strict"| warn["console.warn: skipping rules that reference unset variables"]
  warn --> drop["removeRulesWithMissingEnv"]
  drop --> ok
```

| Command | `strict` | Unset `${VAR_NAME}` |
| --- | --- | --- |
| `plan` | `false` | Warn, drop matching `rules`, continue |
| `apply --dry-run` | `false` | Same as `plan` |
| `apply --yes` | `true` | Throw; no PUT |

Non-strict warning:

```text
vwaffle: warning: skipping rules that reference unset variables: INTERNAL_TOKEN ($.rules[1].conditionGroup[0].conditions[0].value)
```

Strict error (also prefixed `vwaffle: ` on stderr, exit code `1`):

```text
missing environment variables: INTERNAL_TOKEN ($.rules[1].conditionGroup[0].conditions[0].value). Set them before applying the firewall configuration.
```

`removeRulesWithMissingEnv` extracts each missing name as the token before the first space, then drops any `config.rules` entry whose `JSON.stringify` still contains `` `${name}` ``. If `rules` is not an array, or no rule contains those placeholders, the rest of the object is unchanged — including uninterpolated `${VAR_NAME}` strings on `ips` or other non-rule fields. `apply --yes` still fails whenever any placeholder is unset, regardless of where it sits.

## Redaction

Resolved secret **values** (the `secrets` map) are replaced with `[REDACTED]` when:

- `apply --dry-run` prints `redacted(config, secrets)` (tab-indented JSON).
- `apply --yes` prints `redactText(result, secrets)` on the live-versus-desired diff.

Empty-string replacements are not redacted (`if (secret)`). `plan` prints the raw `diff` string and does not run `redactText`. `vwaffle help` states that interpolated values are redacted in all output.

## Context errors

These fire before any Firewall request when `resolveContext` runs (`pull`, `plan`, `apply --yes`):

| Condition | Message |
| --- | --- |
| `VERCEL_TOKEN` unset | `VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.` |
| No project from `--project`, `VERCEL_PROJECT_ID`, or `.vercel/project.json` `projectId` | `A project is required. Pass --project, set VERCEL_PROJECT_ID, or run \`vercel link\` so .vercel/project.json exists.` |

Failed HTTP calls throw `Vercel API ${status} ${statusText}: …` from `request`.

## CI inputs

Drift detection and apply-on-merge both need a token and a project. Pass team when the token is team-scoped.

```yaml
# .github/workflows/firewall.yml
- run: npx vwaffle plan --check
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
    VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
    VERCEL_TEAM_ID: ${{ vars.VERCEL_TEAM_ID }}
```

Export the same names, plus every `${VAR_NAME}` referenced by `firewall.config.json`, before `apply --yes`.

## Related pages

<CardGroup>
  <Card title="Project and team context" href="/project-context">
    How `resolveContext` ranks flags, environment variables, and `.vercel/project.json`.
  </Card>
  <Card title="Secret interpolation" href="/secret-interpolation">
    Placeholder expansion, rule dropping, and `[REDACTED]` output.
  </Card>
  <Card title="Authentication and context errors" href="/authentication-errors">
    Missing `VERCEL_TOKEN`, unresolved project or team IDs, and API failures.
  </Card>
  <Card title="Interpolation and check failures" href="/interpolation-and-check-failures">
    Unset `${VAR_NAME}` on plan versus apply, and `plan --check` exit 1.
  </Card>
  <Card title="Apply from CI" href="/apply-from-ci">
    Token, project, and team inputs for `apply --yes` on merge.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Desired-file path and `loadDesiredConfig` interpolation rules.
  </Card>
</CardGroup>
