---
name: gosuda-testing-relay-server
description: "How to build and run the portal-tunnel relay-server locally for HTTP/browser testing of the embedded SPA frontend and admin/policy APIs."
license: "MIT"
metadata:
  decimalai-skill-id: "adb8a68d-168c-4c30-b4e2-b8a7290cfb4d"
  decimalai-version-id: "5778fb9c-e233-44a4-b7a2-74d161e3d492"
  decimalai-version: "1"
  decimalai-body-sha256: "7565a6bbd1f3e972e72b3f6ec8b0253ee949d1ae5c136b3cd7610c09ad2bb6c9"
  decimalai-name: "gosuda/testing-relay-server"
  decimalai-format: "1"
  decimalai-invocation: "model"
  decimalai-category: "devops"
  decimalai-source: "https://app.decimal.ai/s/gosuda-testing-relay-server@1/SKILL.md"
---
# Testing the relay-server locally

## Build
- Go 1.26+ at `/usr/local/go/bin` (add to PATH); frontend needs Node 22+ for vite.
- `make build-frontend` → populates `cmd/relay-server/dist/app` (required before server build; missing assets make startup fail with a 'make build-frontend' error).
- `make build-server-bin` → `bin/relay-server`.

## Run
```
ADMIN_TOKEN=<token> IDENTITY_PATH=/tmp/pt-id ./bin/relay-server serve --sni-port 8443
```
- Default `--sni-port 443` fails to bind unprivileged — always override.
- API/frontend on https://localhost:4017 (self-signed TLS; use `curl -k`; plain HTTP returns 400).
- Env vars: API_PORT (4017), PORTAL_FRONTEND_DIR (dir-backed SPA, cache disabled → live-editable), LANDING_PAGE_ENABLED, ADMIN_TOKEN.

## Policy API
- `GET/POST /api/policy` with `Authorization: Bearer $ADMIN_TOKEN`.
- POST requires the FULL settings object (e.g. `{"approval_mode":"auto","landing_page_enabled":true,"udp":{"enabled":false,"max_leases":0},"tcp_port":{"enabled":false,"max_leases":0}}`); partial bodies get 400 invalid_mode.
- State persists to `<IDENTITY_PATH>/policy.json`.

## Gotchas
- When killing the server from a shell, use `pkill -x relay-server` — `pkill -f` patterns containing the binary path match your own shell command and kill it.
- Reserved root prefixes that must never serve the SPA: `/api`, `/sdk`, `/discovery`, `/v1` (types.ReservedRootPrefixes in `types/paths.go`).
