# Quickstart

> Set VERCEL_TOKEN, scaffold or pull firewall.config.json, preview with plan, then apply --yes, including the first success signal.

- 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/index.ts`
- `src/config.ts`
- `src/api.ts`
- `README.md`

---

---
title: "Quickstart"
description: "Set VERCEL_TOKEN, scaffold or pull firewall.config.json, preview with plan, then apply --yes, including the first success signal."
---

`vwaffle` reaches a reviewed Firewall `PUT` through `init` or `pull`, `plan`, then `apply --yes`. The default desired file is `firewall.config.json`. `pull`, `plan`, and `apply` require `VERCEL_TOKEN` and a project ID from `--project`, `VERCEL_PROJECT_ID`, or `.vercel/project.json`.

<Info>
`init` writes a local starter file and does not call Vercel. Every other command in this path uses `resolveContext` and the Firewall API.
</Info>

## Prerequisites

| Input | Required for | Resolution (first match wins) |
| --- | --- | --- |
| `VERCEL_TOKEN` | `pull`, `plan`, `apply` | Environment only. Create a token at `https://vercel.com/account/tokens`. |
| Project ID | `pull`, `plan`, `apply` | `--project`, then `VERCEL_PROJECT_ID`, then `.vercel/project.json` `projectId` (`vercel link`) |
| Team ID | Optional API scope | `--team`, then `VERCEL_TEAM_ID`, then `.vercel/project.json` `orgId` |

Install once, or invoke without installing:

<CodeGroup>

```sh title="global install"
npm install -g vwaffle
vwaffle help
```

```sh title="npx"
npx vwaffle help
```

```sh title="bunx"
bunx vwaffle help
```

</CodeGroup>

<ParamField body="VERCEL_TOKEN" type="string" required>
Bearer token sent as `Authorization: Bearer …` on `GET /v1/security/firewall/config/active` and `PUT /v1/security/firewall/config`. Missing token fails before any request.
</ParamField>

<ParamField body="VERCEL_PROJECT_ID" type="string">
Project to target when `--project` is omitted and `.vercel/project.json` is absent.
</ParamField>

<ParamField body="VERCEL_TEAM_ID" type="string">
Optional `teamId` query parameter. Omitted when no flag, env, or linked `orgId` is present.
</ParamField>

## First run

<Steps>
<Step title="Export the API token">

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

`resolveContext` throws if this is unset:

```text
VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.
```

</Step>

<Step title="Point at a project">

Use a linked project, or pass IDs explicitly:

```sh
# after `vercel link` — reads .vercel/project.json
vwaffle plan

# or without a link
export VERCEL_PROJECT_ID=prj_...
export VERCEL_TEAM_ID=team_...   # optional
# equivalent flags: --project ID --team ID
```

Missing project fails with:

```text
A project is required. Pass --project, set VERCEL_PROJECT_ID, or run `vercel link` so .vercel/project.json exists.
```

</Step>

<Step title="Get a desired file">

Pick one. Default path is `firewall.config.json` (`-c, --config FILE` overrides it).

<Tabs>
<Tab title="Scaffold with init">

```sh
vwaffle init
```

Writes tab-indented `STARTER_CONFIG` and prints:

```text
Wrote firewall.config.json. Edit it, then run `vwaffle plan`.
```

`init` refuses to overwrite an existing file:

```text
firewall.config.json already exists; refusing to overwrite.
```

Starter body (`src/index.ts`):

```json
{
	"firewallEnabled": true,
	"managedRules": {
		"owasp": { "active": false }
	},
	"rules": [
		{
			"name": "Block sensitive paths",
			"description": "Deny requests to paths that should never be publicly reachable.",
			"active": true,
			"conditionGroup": [
				{
					"conditions": [{ "type": "path", "op": "pre", "value": "/.git" }]
				}
			],
			"action": { "mitigate": { "action": "deny" } }
		}
	],
	"ips": []
}
```

</Tab>
<Tab title="Pull the live config">

```sh
vwaffle pull --output firewall.config.json
```

`GET`s `/v1/security/firewall/config/active`, writes the active body, and prints:

```text
Wrote active firewall configuration to firewall.config.json.
```

Omit `--output` to print the same JSON on stdout. The file is the exact JSON body accepted by `PUT /v1/security/firewall/config`.

</Tab>
</Tabs>

</Step>

<Step title="Preview with plan">

```sh
vwaffle plan
```

`plan` loads the desired file with `strict: false`, fetches live config, and prints `diff(live, desired)`. Unset `${VAR_NAME}` placeholders warn and drop the rules that still contain those placeholders:

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

For CI, `vwaffle plan --check` sets `process.exitCode = 1` when the printed result is not `NO_DRIFT`.

</Step>

<Step title="Inspect, then apply">

`apply` without `--yes` or `--dry-run` throws and does not call Vercel:

```text
apply requires --yes. Use --dry-run to inspect the payload without calling Vercel.
```

<CodeGroup>

```sh title="inspect redacted payload"
vwaffle apply --dry-run
```

```sh title="PUT the desired config"
vwaffle apply --yes
```

</CodeGroup>

`--dry-run` loads with `strict: false`, prints `redacted(config, secrets)`, and returns. `--yes` loads with `strict: true`, prints the redacted live-versus-desired diff, then `PUT`s only when that diff is not `NO_DRIFT`.

</Step>
</Steps>

## First success signal

On a successful write, `apply --yes` prints the redacted diff, then:

```text
Applied firewall configuration.
```

If the PUT response includes `version`, the same line carries it:

```text
Applied firewall configuration (version 3).
```

That string is the apply success signal. Interpolated secret values appear as `[REDACTED]` in the printed diff and in `--dry-run` JSON.

If the live config already matches the desired file, `apply --yes` still prints the no-drift diff and returns without `PUT`. There is no `Applied firewall configuration` line in that case.

```text
apply
├── missing --yes and --dry-run  →  error, no API call
├── --dry-run                    →  print redacted desired JSON, return
└── --yes
    ├── missing ${VAR}           →  error (strict load)
    ├── diff === NO_DRIFT        →  print diff, no PUT
    └── drift                    →  PUT config  →  Applied firewall configuration[ (version N)].
```

<Check>
First green path: `Wrote firewall.config.json…` or `Wrote active firewall configuration to …`, a `plan` diff, then `Applied firewall configuration` (optionally with a version) after `apply --yes`.
</Check>

## Desired file and secrets

The desired file is the PUT body. String fields may contain `${VAR_NAME}` (`[A-Za-z_][A-Za-z0-9_]*`). Expansion comes from `process.env`; resolved values are tracked as secrets and replaced with `[REDACTED]` in CLI output.

| Command | Missing `${VAR_NAME}` |
| --- | --- |
| `plan`, `apply --dry-run` | Warning, then `removeRulesWithMissingEnv` — rules whose serialized JSON still contain the unset placeholder are dropped so the rest of the plan can run. |
| `apply --yes` | Throws: `missing environment variables: NAME ($.path), …. Set them before applying the firewall configuration.` |

Do not commit the expanded values. Keep placeholders in git and export the variables in the shell that runs `apply --yes`.

<Warning>
`apply --yes` fails closed on unset placeholders. `plan` does not: a local plan can omit rules that CI would reject on apply.
</Warning>

## First-run errors

CLI failures print `vwaffle: <message>` and set `process.exitCode = 1`.

| Symptom | Cause |
| --- | --- |
| `VERCEL_TOKEN is required…` | Token unset. `init` does not need it; `pull` / `plan` / `apply` do. |
| `A project is required…` | No `--project`, `VERCEL_PROJECT_ID`, or `.vercel/project.json` `projectId`. |
| `apply requires --yes…` | `apply` invoked with neither `--yes` nor `--dry-run`. |
| `<file> already exists; refusing to overwrite.` | `init` will not replace an existing desired file. Use `pull --output` or edit in place. |
| `missing environment variables: …` | `apply --yes` hit an unset `${VAR_NAME}`. |
| `Vercel API <status> <statusText>: …` | Non-OK response from `GET /active` or `PUT` on `/v1/security/firewall/config`. |
| `unknown command <name>. Run \`vwaffle help\`.` | First non-flag argument is not `init`, `pull`, `plan`, `apply`, `help`, or `version`. |

`VERCEL_API_URL` overrides the API host when set; the default base is `https://api.vercel.com/v1/security/firewall/config`. Requests always send `projectId`; `teamId` is added only when resolved.

## Next

<CardGroup>
<Card title="Scaffold and pull a config" href="/scaffold-and-pull">
`init` starter file versus `pull --output`, including overwrite behavior.
</Card>
<Card title="Preview and apply changes" href="/preview-and-apply-changes">
`plan` diffs, `apply --dry-run` payloads, and `apply --yes` PUT.
</Card>
<Card title="Project and team context" href="/project-context">
How `resolveContext` picks token, project, and team from flags, env, and `.vercel/project.json`.
</Card>
<Card title="Authentication and context errors" href="/authentication-errors">
Missing token, unresolved project, and Firewall API request failures.
</Card>
</CardGroup>
