Inbound traffic falls into two groups with very different exposure needs:
- Your users — browsers and API clients. Can stay entirely on a private
network/VPN.
- External SaaS webhooks — third-party services pushing events to you.
Require a publicly reachable endpoint.
Services and ports
Self-hosted Noxus puts a reverse proxy (nginx on VM, an ingress controller on
Kubernetes) in front of three HTTP services. Only the proxy needs to be exposed.
| Service | Internal port | Public hostname (typical) | Who connects |
|---|
| Frontend (Next.js) | 8080 (3000 on VM) | app.example.com | Users’ browsers |
| Backend API (FastAPI) | 8080 (8100 on VM) | api.example.com | Browsers, API clients, SSE |
| Relays (FastAPI) | 8080 (5003 on VM) | relay.example.com | External SaaS webhooks |
| Workers | 8080 (internal) | — | Never exposed |
| Plugin server / sandbox | 8500 / 8080 (internal) | — | Never exposed |
On Kubernetes these are three ingress hosts (main, api., relay.) all
routing to internal port 8080. On the VM, nginx terminates TLS on 80/443
and proxies to the per-service ports. Only 443 (and 80 for redirect) needs
to be open at the edge.
Inbound from your users (UI, API, streaming)
These power the product itself and are always required, but they only need
to be reachable by your users — a LAN or VPN address is fine.
- Frontend + Backend API over HTTPS.
- Server-Sent Events (SSE) — long-lived
GET/POST responses with
text/event-stream, used for live run progress (/v1/runs/{run_id}/events)
and agent replies (/v1/conversations/{conversation_id}/stream and
/events). Make sure your proxy does not buffer these responses and
allows long-lived connections (disable response buffering; set a generous
read timeout — streams can run for minutes).
- WebSockets — used by optional interactive features (sandbox shell,
playbook recording). If your proxy needs explicit WebSocket upgrade rules,
add them; these features simply won’t work without them, but core usage is
unaffected.
A common self-hosting bug is an idle-timeout or buffering proxy that cuts SSE
streams. If runs “hang” in the UI but complete server-side, check your proxy’s
buffering and timeout settings on the API host first.
Inbound from external SaaS (channel webhooks)
Agent and trigger channels differ in whether the external service pushes
events to you (needs public inbound) or whether Noxus pulls them (outbound
only). This is the single biggest restricted-networking consideration.
| Channel | Mechanism | Needs public inbound? | Notes |
|---|
| WhatsApp (Cloud API) | Meta POSTs to your webhook | Yes | No polling alternative — webhook is mandatory |
| Microsoft Teams | Teams POSTs change notifications | Yes | No polling fallback |
| Telegram | setWebhook registers your URL; Telegram POSTs updates | Yes | A getUpdates polling fallback is not implemented |
| Generic webhook trigger | External system POSTs to /webhook/{group_id}/{trigger_id} | Yes | The whole point is to receive external calls |
| Google Chat | Webhook or Pub/Sub | Only in webhook mode | Switch to Pub/Sub mode to make it outbound-only |
| Slack | Socket Mode (outbound WS) or Events webhook | No, with Socket Mode | Socket Mode opens an outbound WebSocket to Slack, so no inbound is needed |
| Gmail | Relay polls the Gmail API | No | Outbound polling loop |
| Outlook / email | Relay polls the provider | No | Outbound polling loop |
| Knowledge base sync | Worker polls the source | No | Periodic outbound polling |
In a private/VPN-only deployment you can still use Slack (Socket Mode),
Gmail/Outlook, Google Chat in Pub/Sub mode, and KB sync — they
never need an inbound path. The push channels (WhatsApp, Teams, Telegram,
generic webhooks) require exposing the relay host to the internet; expose
only relay.example.com and keep the app itself private if you want to
minimise surface area.
Securing the relay endpoint
If you must expose the relay for push channels, restrict it: each provider signs
or carries a secret on its webhook (Slack signing secret, Telegram secret token,
WhatsApp/Meta app secret, per-trigger tokens for generic webhooks). Keep the
relay host on its own subdomain so you can apply WAF/rate-limit rules
independently of the app.
OAuth callbacks
When a user connects an integration (Google, Microsoft, Slack, GitHub…), the
provider sends the user’s browser back to:
{BACKEND_URL}/integrations/oauth/callback
This is a 302 redirect in the user’s browser, not a server-to-server call
from the provider. So the callback URL only needs to be reachable by your
users’ browsers — the same audience as the rest of the app. The subsequent
token exchange (backend → the provider’s token endpoint) is outbound (see
Outbound).
Implications:
- If your users reach the app over a VPN, an internal
BACKEND_URL works for
OAuth — no public inbound required.
BACKEND_URL must exactly match the redirect URI registered in each OAuth
app, and the frontend redirect_uri must share the configured FRONTEND_URL
origin (the backend validates this).
- If you can’t expose an OAuth callback at all, fall back to manual
credential entry (static API tokens) for providers that support it.