# Local development

This guide runs the Node control plane on your machine and, optionally, an agent in a local Linux VM. For a remote installation, see [Cloudflare](/docs/cloudflare-deployment/) or [Docker Compose](/docs/production-operations/). For code structure and plugins, see [Development](/docs/development/).

## Requirements

- Node.js 22.12+ and pnpm 10.17+.
- Go 1.27.3, pinned in `agent/go.mod`, to build or test the agent.
- macOS and [Lima](https://lima-vm.io/docs/installation/) for the VM workflow.

## Start the control plane

```sh
pnpm install
pnpm configure
pnpm dev
```

Choose **Local development**. Setup writes `app/.env`, generates secrets and can create a GitHub App. `pnpm dev` applies pending migrations to the databases in `app/.data/`, and stops if a migration fails. If you reuse a database, keep its existing encryption key.

Open `http://localhost:7429`. Startup fails if the port is in use. Configure [sign-in](/docs/configuration/#authentication). The first account becomes administrator unless `DEVPUSH_ADMIN_EMAIL` is set. Node development does not start the background scheduler; see [Background work](/docs/production-operations/#background-work).

## Local deployment VM

```sh
pnpm vm:setup
pnpm agent:build:linux
```

`vm:setup` creates and starts the `devpush-test` VM with Docker. The VM has 2 CPUs, 4 GiB of memory, a 30 GiB disk and VZ/vzNAT networking. Set these values in `app/.env` and restart the app:

```env
DEVPUSH_BASE_URL=http://host.lima.internal:7429
BETTER_AUTH_URL=http://localhost:7429
DEVPUSH_MCP_URL=http://localhost:7429/mcp
DEVPUSH_AGENT_BINARY_DIR=/absolute/path/to/devpush/bin
```

The VM reaches your machine through `host.lima.internal`, while the browser keeps using localhost. Add a server in devpush and pass the URL from its generated install command:

```sh
pnpm vm:install 'http://host.lima.internal:7429/api/install/agent/INSTALL_TOKEN'
```

This installs the agent and pins the VM's private IP with `--address`. When the server connects, create a project. Visit URLs use HTTP and nip.io, such as `http://my-app-192-168-64-7.nip.io`. DNS resolvers that block private-address answers may also block nip.io.

If the VM's IP changes, update `DEVPUSH_ROUTING_ADDRESS` in the VM's `/etc/devpush/agent.env` and restart `devpush-agent`. Existing aliases are not rewritten; see [Routing addresses](/docs/servers/#routing-addresses).

Helper commands:

- `pnpm dev:start`, `pnpm dev:stop` and `pnpm dev:restart` manage the app in the background. The log is `.tmp/devpush-dev.log`.
- `dev:start` also starts the VM, unless you set `DEVPUSH_REQUIRE_LIMA=0`. `DEVPUSH_LIMA_INSTANCE=NAME` selects another VM.
- `pnpm vm:stop` stops the VM.
- `pnpm vm:destroy` deletes the VM and its data. It does not remove the server record from the dashboard.

## Tests

Run tests from the repository root. Tests use temporary stores; never point them at live data. Start with the narrowest check for your change:

| Command | Coverage |
| --- | --- |
| `pnpm typecheck` / `pnpm lint` | TypeScript and lint rules |
| `pnpm test` | App unit/DOM, setup, VM and script tests |
| `pnpm test:integration` | Persistence against isolated SQLite/D1 |
| `pnpm --dir app test:integration:libsql` | Services through libSQL |
| `pnpm agent:test` | Go agent |
| `pnpm build` | Production Node build |
| `pnpm test:package` | Installed tarball on Node and Workers |
| `DEVPUSH_WRANGLER_CONFIG=wrangler.template.jsonc pnpm --dir app build:cloudflare` | Worker build from the template |
| `pnpm audit:dependencies` | Dependency vulnerabilities |
| `pnpm docs:check` | Documentation navigation and links |

CI workflows define the automated gates. A configured workflow does not prove that a given revision passed.

### Schema changes

```sh
pnpm --dir app db:generate
pnpm --dir app db:generate:logs
pnpm --dir app db:migrate
pnpm test:integration
```

`db:migrate` applies both main and log stores. Generate only the migration sets you changed, and commit the SQL with its Drizzle metadata. Never rewrite an applied migration. Use `db:push` only for disposable local experiments.

### VM and smoke tests

`pnpm vm:test` runs real Compose deployment, Traefik routing and HTTPS certificate tests against `devpush-test`. It refuses an enrolled VM. If you have used the VM manually, preserve any data you need and run `pnpm vm:destroy` first. `pnpm smoke:agent` runs the same tests against another isolated Docker host. Never use a host that serves other agents.

Other checks:

- `pnpm validate:local` runs the routine checks plus deployment and Docker routing smoke tests. Set `DEVPUSH_SKIP_DOCKER_SMOKE=1` only to deliberately skip the routing smoke test.
- `pnpm build && pnpm smoke:node`, `pnpm smoke:dev` and `pnpm --dir app smoke:worker d1` run isolated Node, development and Worker servers.
- `pnpm validate:production` builds and checks the Compose image and stack. It requires Docker.
- `pnpm smoke:deploy` creates fixture data in the **current** database configuration. Run it only against isolated stores.
- `pnpm validate:remote` and `pnpm validate:remote:proof` install and verify an agent on a dedicated test host. Read `pnpm validate:remote -- --help` first, and treat the install command as a credential.
- `pnpm setup:hetzner -- --list` lists server options. Only `--create` makes a billable server. `--destroy` removes tracked resources.

Local simulators do not prove real provider credentials, DNS or GitHub webhooks.

## Troubleshooting

- **Agent disconnected:** From the VM, request `http://host.lima.internal:7429/api/health`. Check the laptop listener and the URL in `/etc/devpush/agent.env`.
- **Visit fails:** Confirm the VM IP and the route's service, port and exact hostname. DNS resolution alone does not configure Traefik.
- **OAuth fails:** Check the registered origin and callback against [Authentication](/docs/configuration/#authentication).
