> ## Documentation Index
> Fetch the complete documentation index at: https://usenaive.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AP-mode onboarding (headless WiFi setup)

> First-boot WiFi setup for a headless Tower — the box brings up its own AP + captive portal so a phone can hand it the home network.

A Tower with a screen or an Ethernet cable is easy to onboard. A **headless,
WiFi-only** box has a chicken-and-egg problem: it needs WiFi credentials before
it can be reached. AP-mode onboarding solves it — on first boot with no saved
network the box brings up its own access point and captive portal.

## The flow

1. **No saved network** → the box starts an AP named `Naive-XXXX`
   ([balena wifi-connect](https://github.com/balena-os/wifi-connect), which
   drives NetworkManager over DBus and runs a `dnsmasq` captive portal).
2. Your **phone joins** `Naive-XXXX`; the OS captive-portal detection opens the
   portal (served by wifi-connect, or the box's own page at
   `GET /v1/device/portal`).
3. The portal **scans** nearby WiFi (`GET /v1/device/network/scan`) and you pick
   your home network + enter the password.
4. The box **joins** it (`POST /v1/device/network/join` → NetworkManager
   `nmcli dev wifi connect`), the **AP tears down**, and the box comes up on your
   LAN.
5. You continue in the normal [onboarding wizard](/docs/self-hosting/overview) at
   `http://tower.local` — this AP step runs *before* Step 1.

## Endpoints (self-hosted only, inert in cloud)

| Route                          | Purpose                                                       |
| ------------------------------ | ------------------------------------------------------------- |
| `GET /v1/device/network/scan`  | List visible WiFi networks for the portal picker.             |
| `POST /v1/device/network/join` | Join `{ ssid, psk }`; NetworkManager connects, AP tears down. |
| `GET /v1/device/portal`        | The self-contained captive-portal HTML page.                  |

All three are gated to `NAIVE_BUILD=tower` and 404/return empty in cloud.

## Install

The AP stack is opt-in (it needs AP-capable WiFi hardware and would disrupt a box
already on the LAN):

```bash theme={"theme":"css-variables"}
NAIVE_AP_ONBOARDING=1 sudo ./install.sh
```

This installs `network-manager` + `dnsmasq-base` and fetches the balena
wifi-connect binary for the box's arch (`aarch64` or `x86_64`). wifi-connect
needs `NET_ADMIN` and an AP-capable WiFi adapter.

## What is VM-testable vs. hardware-gated

<Note>
  The **web flow + join endpoint are fully testable in a VM** with no radio: set
  `NAIVE_WIFI_MODE=mock` (or just run a non-Tower build) and the scan returns a
  synthetic network list and join returns a synthetic success — so the captive
  portal page and `/network/join` can be exercised end-to-end in CI.

  The **hardware leg is genuinely not VM-testable** — bringing up a real AP,
  serving the captive portal over it, and joining a real WiFi radio require
  AP-capable WiFi hardware. That leg is validated on a real device.
</Note>

## Real-device checklist

Run these on real AP-capable hardware (marked **BLOCKED** until validated on a
device — do not assume they pass from the VM subset):

* [ ] Boot headless with no saved WiFi → AP `Naive-XXXX` appears.
* [ ] Phone joins the AP → captive portal auto-opens (iOS + Android).
* [ ] Portal scan lists real nearby SSIDs (`nmcli dev wifi list` parity).
* [ ] Selecting a network + password joins it (`nmcli dev wifi connect`).
* [ ] The AP tears down and the box is reachable at `http://tower.local`.
* [ ] A wrong password surfaces a clear error and the AP stays up for a retry.
* [ ] Reboot after a successful join reconnects automatically (no AP).
