# Local development setup (hybrid: local UI + hosted controller)

This documents how to get zrok running on a local macOS box for UI/extension development, without standing up a
full self-hosted backend (OpenZiti network, Postgres, RabbitMQ, InfluxDB, etc.). The approach: build and run
the CLI and the web console **locally**, but point them at NetFoundry's **hosted** controller
(`https://api-v2.zrok.io`) for account/environment/share data. Full self-hosting is a separate, much larger
effort — see `website/docs/self-hosting/deployment/`.

## What ends up running where

| Piece                                    | Runs where                                      |
|------------------------------------------|--------------------------------------------------|
| `zrok2` CLI binary                       | local (built from source)                       |
| Web console (`ui/`)                      | local (Vite dev server, live source)            |
| Account, environment, share data         | hosted (`api-v2.zrok.io`)                       |
| Share traffic (once you create a share)  | OpenZiti overlay (peer-to-peer/hosted frontend) |

## Prerequisites and dependencies installed

Building zrok requires **Go**, **Node.js v18+**, and a **C compiler** (CGO is required because the controller
uses SQLite3). On a fresh macOS box, expect to install:

1. **Go** (`go.mod` currently requires Go 1.26+; verified working with 1.27.1):

   ```bash
   brew install go
   ```

2. **A C compiler**, via Xcode Command Line Tools. On some systems `xcode-select -p` reports a path even
   though nothing is actually installed (`xcrun` fails, `pkgutil --pkg-info=com.apple.pkg.CLTools_Executables`
   finds no receipt). If the normal popup (`xcode-select --install`) or **System Settings → General →
   Software Update** doesn't offer it, install directly via `softwareupdate`, no Apple ID needed:

   ```bash
   softwareupdate -l                                              # find the exact label first
   sudo softwareupdate --install "Command Line Tools for Xcode 26.6-26.6"   # ~900MB, no restart needed
   ```

   Verify with `xcrun --version` (should print a version, not an error) and `cc --version`.

3. **Node.js v18+** — already required for any other frontend work in this repo (nothing extra to install if
   you already build other JS/TS projects on this box).

No database, message queue, or OpenZiti network needs to be installed for this hybrid approach — those are
only needed for self-hosting the controller.

## Step-by-step setup

```bash
cd /path/to/zrok

# 1. Build the web console UI (installs ~320 npm packages into ui/node_modules)
(cd ui && npm install && npm run build)

# 2. Build the agent's local UI (installs ~250 npm packages into agent/agentUi/node_modules)
(cd agent/agentUi && npm install && npm run build)

# 3. Build the zrok2 binary (this also downloads the Go module graph on first run — large,
#    OpenZiti pulls in a lot of transitive dependencies)
mkdir -p dist
go build -o dist ./cmd/zrok2

# 4. Verify
./dist/zrok2 version
./dist/zrok2 --help
```

### Create/enable your account

```bash
./dist/zrok2 invite
```

This is an interactive terminal form — enter your email, submit. You'll receive an email from zrok.io with an
account-creation link. Under the hood: the hosted controller stores an `AccountRequest{token, email}` row and
emails you a link containing that token, pointing at the **hosted** web console (NOT your local build — see
"What actually gets served where" below). Click it, set a password; that calls the hosted
`AccountRegisterHandler`, which creates the `Account` row and returns an **account token**.

```bash
./dist/zrok2 enable <account-token>
```

This provisions a local OpenZiti identity and writes `~/.zrok/environment.json`. Verify with:

```bash
./dist/zrok2 status
```

You should see `apiEndpoint: https://api-v2.zrok.io`, and `Account Token` / `EnvZId` both `<<SET>>`.

### What actually gets served where (important gotcha)

The `ui/dist` build you produce locally is **embedded into the controller binary** (`ui/embed.go`), and only
ever gets served by a running `zrok2 controller` process (`ui.Middleware()` wraps the controller's HTTP
handler in `rest_server_zrok/configure_zrok.go`). The client command `zrok2 console` does **not** serve your
local build — it just opens your browser at whatever `apiEndpoint` your environment points to
(`cmd/zrok2/console.go`). Since that's the hosted controller, `zrok2 console` opens NetFoundry's own deployed
console, not your local one.

To actually see and develop against **your own** `ui/` source locally, run its Vite dev server directly
(step below) rather than relying on `zrok2 console`.

### Run the local UI, pointed at the hosted API

`ui/vite.config.ts` proxies API calls under `/api/v2` (the same base path the hosted API uses) to a target
host. By default it's configured for a **self-hosted** local controller:

```ts
proxy: {
  '/api/v2': {
    target: 'http://localhost:18080',
    changeOrigin: true,
  }
}
```

For the hybrid setup, repoint it at the hosted controller:

```ts
proxy: {
  '/api/v2': {
    target: 'https://api-v2.zrok.io',
    changeOrigin: true,
    secure: true,
  }
}
```

Then start the dev server:

```bash
cd ui
npm run dev
```

Open **<http://localhost:5173/>** — this is your local `ui/` source, live-reloading, running in your browser.
Log in with the email/password you registered above; it now talks to the real hosted API, so your actual
account/environment/share data shows up.

> **Note:** the `vite.config.ts` proxy change above is a local-only dev tweak — it will show up as an
> uncommitted change (`git status`) and shouldn't be committed as-is, since it would break the default
> self-hosted-dev experience for others.

### Create a share (CLI, real running process)

```bash
./dist/zrok2 share public localhost:8080   # or whatever's running locally
```

This is a genuine local process built from source, tunneling traffic over the Ziti overlay via the hosted
controller's frontend.

---

## Extension integration

zrok's `ui/` (on the `v2.0_ui_extensions` branch) has a plugin system for adding features to the console
without forking it — nav items, routes/pages, side-panel extensions, slot injections, lifecycle hooks
(`onUserLogin`/`onUserLogout`), and build-time `<head>`/`<body>` script injection.

- **Framework code**: `ui/src/extensions/` (`registry.ts`, `types.ts`, `context.ts`, `SlotRenderer.tsx`, etc.)
- **Registration entry point**: `ui/src/extensions.config.ts` — extensions are statically imported and
  registered here via `extensionRegistry.register(manifest)`, called from `loadExtensions()` in `App.tsx`.
- **Example**: `ui/examples/demo-extension/` shows the pattern for a local-path extension.

An extension is a separate npm package that exports a default object implementing `ExtensionManifest`
(`ui/src/extensions/types.ts`). It's built as a library — React, MUI, `@emotion/*`, `react-router`, and
`@xyflow/react` must be marked `external` in its bundler config so it shares the host `ui/`'s instances
rather than bundling duplicates (which would break, e.g., React hooks).

### Worked example: `@netfoundry/myzrok-extension` (billing)

Source lives in a sibling repo, e.g. `/path/to/myzrok-extension-develop` (private — adds a "Billing" nav item
and `/billing` page, Chargebee-backed, talking to a separate `myzrok-orchestration` backend service that is
**not** part of this repo).

1. **Build the extension** (produces `dist/bundle.min.js` + `dist/index.d.ts` via Rollup):

   ```bash
   cd /path/to/myzrok-extension-develop
   npm run build
   ```

2. **Install it into `ui/` as a local file dependency** (pulls in the extension's own non-peer dependencies —
   Joi, Chargebee's React wrapper, etc. — these are already bundled into `bundle.min.js`, so nothing extra is
   needed beyond adding the package itself):

   ```bash
   cd ui
   npm install ../../myzrok-extension-develop
   ```

   This adds `"@netfoundry/myzrok-extension": "file:../../myzrok-extension-develop"` to `ui/package.json`.

3. **Register it** in `ui/src/extensions.config.ts`:

   ```ts
   import myzrokExtension from '@netfoundry/myzrok-extension';

   export function loadExtensions(): void {
     extensionRegistry.register(myzrokExtension);
     // ...
   }
   ```

4. **Restart the Vite dev server** so it picks up the new dependency and config:

   ```bash
   # kill any running `npm run dev`, then:
   npm run dev
   ```

5. **Verify**: refresh `http://localhost:5173/`, log in, and confirm a **"Billing"** nav item appears and
   `/billing` renders the extension's `MyzrokDashboard` page.

### Known limitation (local dev)

The billing extension calls its backend (`myzrok-orchestration`) at a **relative** path,
`/myzrok-orchestration/v1/...` (see the extension's own `src/api/runtime.ts` `BASE_PATH`), with no host
configured. Locally this resolves against `localhost:5173` (the Vite dev server), which has no such backend —
requests fall through to Vite's SPA fallback (serving `index.html`, so you'll see a `304`/HTML response
instead of JSON). The extension's wiring (nav → route → component → API call) is confirmed working; only the
billing *data* won't load without a real `myzrok-orchestration` endpoint. If a dev/staging URL for that
service is available, add a second Vite proxy rule for `/myzrok-orchestration`, the same way `/api/v2` was
repointed above.

### Re-registering after extension source changes

Rebuilding the extension (`npm run build` in the extension repo) regenerates `dist/bundle.min.js` in place;
since it's a `file:` dependency, `ui/`'s Vite dev server picks up the new file on the next request/reload —
no need to re-run `npm install` in `ui/` unless the extension's `package.json` itself changed.
