Architecture
Services and public names
| Service | Public name | Container / port | Role |
|---|---|---|---|
| Go API | api-towers.camelcreatives.com | api:8000/TCP | REST endpoints, provider callbacks, business rules |
| Guest portal | wateja-towers.camelcreatives.com | portal:3000/TCP | Public plan selection, payment polling, UAM browser flow |
| Admin | towers.camelcreatives.com | admin:3000/TCP | Staff-only management console |
| Documentation | docs-towers.camelcreatives.com | docs:3000/TCP | Public read-only handbook |
| PostgreSQL | private | db:5432/TCP | Persistent records |
| RADIUS auth | API VPS public IP | 1812/UDP | AP Access-Request / Access-Accept |
| RADIUS accounting | API VPS public IP | 1813/UDP | AP accounting Start, Interim, Stop |
| CoA | AP reachable address | 3799/UDP | API sends Disconnect-Request |
The Camel Creatives VPS uses host Nginx to terminate HTTPS and route the four names to loopback-bound Compose containers. The optional Caddy Compose profile is for a fresh host without an existing reverse proxy. RADIUS is UDP and travels directly to the VPS; neither Nginx nor Caddy proxies those packets.
Trust boundaries
- Provider keys and webhook secrets exist only in the Go API environment.
- The admin UI calls the API with a secure, HttpOnly staff session cookie.
- Public purchase status can be queried by invoice ID. Wi-Fi credentials are only returned when the caller also supplies the matching high-entropy purchase key.
- The guest Next.js server has a private Docker-network proxy to the Go API at
http://api:8000. - The AP can send RADIUS only from source CIDRs configured by
RADIUS_CLIENT_CIDRS; the RADIUS shared secret must match. - PostgreSQL has no published host port in the standard Compose file.
Network diagram
Request paths
Guest purchase
- The browser loads active plans from the guest portal’s same-origin
/api/v1route proxy. - The Go API creates a guest, invoice, and pending payment using the request idempotency key.
- The API calls Abliner to send a payment approval prompt to the guest’s mobile number.
- Abliner sends a signed webhook to
https://api-towers.camelcreatives.com/webhooks/abliner. - The API validates the signature and invoice amount, then commits the paid state and access grant together.
- The browser polls the invoice using its private purchase key. When paid, the UAM path returns a router login URL.
AP authentication
The captive browser submits the CoovaChilli challenge data to the API, receives the router’s local /logon URL, and returns to the AP. CoovaChilli then sends RADIUS authentication to the Go service. The API accepts a matching active grant and returns its remaining time as Session-Timeout.
Current status signal
The API marks a registered router online when it receives an Access-Request or accounting packet with its registered NAS ID. This is RADIUS activity, not a separate heartbeat. The API does not currently age a router to offline after an inactivity timeout; dashboard “online” counts can therefore remain stale until a later status update or manual change.
Separate apps in one deployment
The source lives in one repository and Compose stack, but each web app and the Go API is built into its own container. This keeps deployment coordinated while preserving separate runtime boundaries and public domains.