# Project and team context

> How resolveContext selects VERCEL_TOKEN, project ID, and team ID from flags, environment variables, and .vercel/project.json.

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

---

---
title: "Project and team context"
description: "How resolveContext selects VERCEL_TOKEN, project ID, and team ID from flags, environment variables, and .vercel/project.json."
---

`resolveContext` in `src/api.ts` builds the `ResolvedContext` used by every Vercel Firewall API call. It reads `VERCEL_TOKEN` from the process environment, then selects `projectId` and optional `teamId` with first-match `??` precedence: CLI overrides, then environment variables, then `.vercel/project.json` from `vercel link`.

## When resolveContext runs

`parseArgs` stores `--project` and `--team` on `CliOptions`. `cmdPull`, `cmdPlan`, and confirmed `cmdApply` pass that object into `resolveContext(options)`. Extra CLI fields (`command`, `config`, `--yes`, `--check`, `--output`) are ignored.

| Command | Calls `resolveContext` | Notes |
| --- | --- | --- |
| `pull` | Yes, first | Context is required before `getActiveConfig` |
| `plan` / `plan --check` | Yes, after `loadDesiredConfig` | A missing desired file fails before token or project resolution |
| `apply --yes` | Yes, after loading the desired config | Used for `getActiveConfig` and `putConfig` |
| `apply --dry-run` | No | Prints the redacted payload and returns without contacting Vercel |
| `init`, `help`, `version` | No | Local file or usage output only |

<Note>
`apply --dry-run` does not require `VERCEL_TOKEN`, a project ID, or a team ID. Any other `pull` / `plan` / `apply` path that reaches the API does.
</Note>

## ResolvedContext

```ts
interface ResolvedContext {
  token: string;
  projectId: string;
  teamId?: string;
}
```

<ParamField body="token" type="string" required>
Value of `process.env.VERCEL_TOKEN`. Sent as `Authorization: Bearer ${context.token}` on every Firewall request.
</ParamField>

<ParamField body="projectId" type="string" required>
Target Vercel project. Always appended as the `projectId` query parameter.
</ParamField>

<ParamField body="teamId" type="string">
Optional team scope. When truthy, appended as the `teamId` query parameter. Omitted from the URL when unset or empty.
</ParamField>

## Precedence

`resolveContext(overrides)` uses nullish coalescing (`??`), so only `null` and `undefined` fall through. Empty strings do not.

```text
token     : VERCEL_TOKEN
projectId : overrides.project  ??  VERCEL_PROJECT_ID  ??  .vercel/project.json#projectId
teamId    : overrides.team     ??  VERCEL_TEAM_ID     ??  .vercel/project.json#orgId
```

| Field | 1. CLI flag | 2. Environment | 3. Linked project | Required |
| --- | --- | --- | --- | --- |
| Token | none | `VERCEL_TOKEN` | none | Yes |
| Project | `--project ID` | `VERCEL_PROJECT_ID` | `projectId` | Yes |
| Team | `--team ID` | `VERCEL_TEAM_ID` | `orgId` | No |

There is no `--token` flag and no fallback to Vercel CLI login credentials. Team resolution maps the linked file's `orgId` key onto `ResolvedContext.teamId`.

<Warning>
`VERCEL_PROJECT_ID=` (empty) wins over `.vercel/project.json` and then fails the required-project check. `VERCEL_TEAM_ID=` wins over `orgId` and is treated as no team when building the request URL (`if (context.teamId)`).
</Warning>

## Token

`VERCEL_TOKEN` is the only accepted credential.

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

A missing or empty token throws before the linked-project file is considered:

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

`main` prefixes CLI errors with `vwaffle:` and sets `process.exitCode = 1`:

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

## Project ID

<ParamField body="--project" type="string">
Explicit project ID. `parseArgs` requires a following value (`--project requires a value`).
</ParamField>

<ParamField body="VERCEL_PROJECT_ID" type="string">
Used when `--project` is omitted.
</ParamField>

<ParamField body="projectId" type="string">
Read from `process.cwd()/.vercel/project.json` when both the flag and env var are unset.
</ParamField>

If all three sources miss, `resolveContext` throws:

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

Help text for `--project` documents the same three sources: flag, `VERCEL_PROJECT_ID`, or `.vercel/project.json`.

## Team ID

Team scope is optional. Personal-account projects can omit `--team`, `VERCEL_TEAM_ID`, and `orgId`.

<ParamField body="--team" type="string">
Explicit team ID. Requires a following value (`--team requires a value`).
</ParamField>

<ParamField body="VERCEL_TEAM_ID" type="string">
Used when `--team` is omitted. Help text: "Team scope for the API token".
</ParamField>

<ParamField body="orgId" type="string">
Read from `.vercel/project.json`. This is the Vercel link field name; `resolveContext` stores it as `teamId`.
</ParamField>

No error is raised when every team source is missing. `requestUrl` simply skips the `teamId` query parameter.

## .vercel/project.json

`resolveContext` always attempts to load the linked project from the current working directory, even when flags or env vars already supply IDs.

| Item | Value |
| --- | --- |
| Path | `resolve(process.cwd(), '.vercel/project.json')` |
| Reader | `readJson` (`readFile` + `JSON.parse` in `src/config.ts`) |
| Fields used | `projectId`, `orgId` |
| Missing / invalid file | Caught; `linked` stays `{}` |
| Git | `.vercel/` is listed in `.gitignore` |

```json
{
  "projectId": "prj_xxxxxxxxxxxxxxxxxxxxxxxx",
  "orgId": "team_xxxxxxxxxxxxxxxxxxxxxxxx"
}
```

Any other keys written by `vercel link` are ignored. A missing file is not an error when `--project` / `VERCEL_PROJECT_ID` already provide a project.

<Info>
The catch comment in `resolveContext` states that a linked project is optional when explicit flags or environment variables are set. The file is still opened on every call; only a thrown `readJson` failure (ENOENT, parse error, permissions) is swallowed.
</Info>

## How context reaches the Firewall API

`getActiveConfig` and `putConfig` both call `request`, which builds the URL from the resolved IDs:

```ts
url.searchParams.set('projectId', context.projectId);
if (context.teamId) url.searchParams.set('teamId', context.teamId);
```

The API root is `${VERCEL_API_URL ?? 'https://api.vercel.com'}/v1/security/firewall/config`. `VERCEL_API_URL` is not selected by `resolveContext`; it only changes the host used after context exists.

<RequestExample>
```sh
# Linked project in cwd (.vercel/project.json)
export VERCEL_TOKEN=...
vwaffle plan

# CI / unlinked checkout
export VERCEL_TOKEN=...
export VERCEL_PROJECT_ID=prj_...
export VERCEL_TEAM_ID=team_...
vwaffle plan --check

# Flag override of a linked or env-selected project
vwaffle pull --project prj_other --team team_other --output firewall.config.json
```
</RequestExample>

## Local versus CI

<Steps>
<Step title="Local linked checkout">
Run `vercel link` so `.vercel/project.json` contains `projectId` and, for team projects, `orgId`. Export `VERCEL_TOKEN`. Omit `--project` / `--team` unless targeting a different project than the link file.
</Step>
<Step title="Unlinked or CI checkout">
Pass IDs as environment variables or flags. `.vercel/` is gitignored, so CI cannot rely on a committed link file.

```yaml
- 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 }}
```
</Step>
<Step title="Verify the target">
A successful `pull` or `plan` that reaches Vercel used the resolved `projectId` (and `teamId` when set). Token, project, and HTTP failures print `vwaffle: …` and exit `1`.
</Step>
</Steps>

## Context errors

| Condition | Message | Exit |
| --- | --- | --- |
| `VERCEL_TOKEN` unset or empty | `VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.` | `1` |
| No `--project`, no `VERCEL_PROJECT_ID`, and no usable `projectId` | `A project is required. Pass --project, set VERCEL_PROJECT_ID, or run \`vercel link\` so .vercel/project.json exists.` | `1` |
| `--project` or `--team` with no value | `--project requires a value` / `--team requires a value` | `1` |
| API request fails after context resolves | `Vercel API ${status} ${statusText}: …` from `request` | `1` |

Missing team is not an error. Failed Firewall responses after a valid `ResolvedContext` are covered on the authentication-errors page.

## Related pages

<CardGroup>
<Card title="Environment variables" href="/environment-variables">
`VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, and interpolated `${VAR_NAME}` values.
</Card>
<Card title="Firewall API client" href="/firewall-api-client">
How `resolveContext`, `requestUrl`, `getActiveConfig`, and `putConfig` call Vercel.
</Card>
<Card title="Authentication and context errors" href="/authentication-errors">
Missing token, unresolved project, and failed Firewall requests.
</Card>
<Card title="Apply from CI" href="/apply-from-ci">
Promote `firewall.config.json` with `apply --yes` plus project, team, and token inputs.
</Card>
<Card title="CLI reference" href="/cli-reference">
`--project`, `--team`, and the commands that consume them.
</Card>
</CardGroup>
