---
title: Local development
description: A fast loop with real decisions — simulated execution by default, a real downstream when you want one.
---

The goal locally is real decisions with harmless execution. Veefore gives you
that by default: the pipeline runs for real and the tool runtime is simulated
until you deliberately turn egress on.

## Start the stack

```bash
npm run dev
```

| | |
| --- | --- |
| Control plane | `http://localhost:4000` |
| Gateway | `http://localhost:4010` |
| Console | `http://localhost:5173` |

Full setup in [Self-host Veefore](/install).

## Seed something to look at

```bash
npm run seed:demo
```

Creates an organization with agents, tools, policies, delegations, credentials,
approvals, incidents and audit history — enough that every console page renders
something real. The accounts are printed at the end; the owner is
`owner@acme.test`.

`npm run seed` gives you an empty organization instead.

:::warning Reseeding destroys what you created
`seed:demo` resets the dataset. Anything you set up by hand goes with it. If you
have a working configuration you care about, script it — see
`examples/setup-real.ts` for the pattern, which provisions everything through
the public API.
:::

## Execution is simulated by default

`GATEWAY_ALLOW_REAL_EGRESS=false` is the default, and with it the tool runtime
produces deterministic simulated responses. The full pipeline still runs: real
authentication, real policy, real risk, real approvals, real audit. Only the
outbound call is simulated.

**Every simulated response carries `simulated: true`.** Nothing is presented as
output from a real system when it is not.

That default means you cannot accidentally refund a real customer while testing
a policy. It also means you cannot catch a credential or destination problem,
because nothing is actually called.

## Then make it real, safely

The repository ships a genuine downstream service for exactly this.

```bash
node examples/real-tool-server.mjs
```

A real HTTP service on loopback `:4500` with real mutable state and its own API
key:

| | |
| --- | --- |
| `POST /refunds` | Requires `x-api-key`. Appends to a ledger |
| `GET /ledger` | Open. Shows what actually happened |

Turn egress on:

```ini
GATEWAY_ALLOW_REAL_EGRESS=true
TOOL_SERVER_PORT=4500
TOOL_SERVER_API_KEY=downstream-secret-key
```

Then:

```bash
npx tsx examples/setup-real.ts      # provisions everything through the public API
npx tsx examples/run-real-agent.ts --enforce
curl -s http://localhost:4500/ledger
```

The ledger is the point. It shows you what *actually* reached the downstream,
which is the only way to confirm enforcement did what the decision said it did.
See [Examples](/build/examples).

:::note Plaintext HTTP is allowed to loopback only
The egress guards require HTTPS unless the host is a loopback address. That is
why the example service runs on `127.0.0.1` — a plaintext endpoint on any other
host is refused with `DESTINATION_BLOCKED`.
:::

## Watch decisions live

```bash
veefore dev
```

Streams every decision for this agent with the policy and rule behind it, and
the next concrete step for each outcome. `--all` for every agent in the
environment.

```bash
veefore logs --follow --decision BLOCK
veefore status
```

## Use SIMULATE, not OBSERVE

For a development environment, `SIMULATE` is the better mode:

```bash
veefore deploy --mode SIMULATE --reason "Local development"
```

`SIMULATE` gives you real refusals with no execution, so your error handling is
exercised. `OBSERVE` lets everything through, which means your code never sees a
refusal locally and then sees its first one in production.

## Iterating on policy

```bash
veefore policies test
veefore policies diff pol_01ABC --from 3 --to 4
```

Simulation is the fastest loop, because it has no side effects at all:

```bash
curl -X POST http://localhost:4000/v1/policies/simulate \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{ "agentId": "agt_01JAV…", "tool": "refund_customer", "amount": 150000 }'
```

## Ports

`4000` and `5173` are commonly taken, so expect to change at least one. Change it
in `.env` — everything reads from there, including the console's Vite config.

| Variable | Default |
| --- | --- |
| `CONTROL_PLANE_PORT` | `4000` |
| `GATEWAY_PORT` | `4010` |
| `VEEFORE_CONSOLE_PORT` | `5173` |
| `VEEFORE_CONSOLE_PREVIEW_PORT` | `4173` |
| `VEEFORE_DOCS_PORT` | `4800` |
| `TOOL_SERVER_PORT` | `4500` |

Changing the console port means changing `CORS_ALLOWED_ORIGINS` to match, or the
browser refuses every request.

## Avoid two traps

**Ambient `VEEFORE_*` variables.** A shell with `VEEFORE_AGENT_ID` exported from
an earlier session will quietly make a test authenticate as the wrong agent.
Shell environments persist. If something is behaving as though it is a different
agent, check `env | grep Veefore`.

**The embedded database.** PGlite is per-process. Pointing the stack at it gives
the control plane, the gateway and the seed script each a private copy of the
database, and the symptom is that an agent you just created does not exist. Use
a real PostgreSQL for anything multi-process.

## Related

:::cards

:::card Examples | /build/examples
Four working programs, including the real downstream.
:::

:::card Self-host Veefore | /install
First-time setup.
:::

:::card Troubleshooting | /operate/troubleshooting
The failures people actually hit.
:::

:::card Configuration | /reference/configuration
Every environment variable. Generated.
:::

:::
