InfraCanvas Architecture

May 8, 2026 · View on GitHub

Living document. Update as you build. Replaces codebase re-analysis each session.


Overview

InfraCanvas is a multi-VM infrastructure discovery and visualization platform. Two distinct products share this repo:

  • OSS (infracanvas CLI) — self-hosted, single-binary, Cloudflare tunnel
  • SaaS (saas-backend/ + SaaS frontend) — multi-tenant, hosted on bytestrix VM

System Architecture

┌─────────────────────────────────────────────────────────┐
│                      BROWSER                            │
│  Next.js 14 SPA — React Flow canvas, Zustand state      │
└──────────────┬──────────────────────────────────────────┘
               │ WebSocket /ws/canvas   HTTP /api/v1/*

┌──────────────▼──────────────────────────────────────────┐
│              RELAY / API SERVER (bytestrix)              │
│                                                         │
│  OSS mode:  pkg/server  (:8080, in Docker container)    │
│  SaaS mode: saas-backend/pkg/api  (:8090, systemd)      │
│                                                         │
│  Postgres (port 5432)  Redis (port 6379)                │
└──────────────┬──────────────────────────────────────────┘
               │ WebSocket /ws/agent

┌──────────────▼──────────────────────────────────────────┐
│              AGENT  (on each VM)                        │
│  pkg/agent — discovers host/docker/k8s, streams diffs   │
└─────────────────────────────────────────────────────────┘

OSS Stack

Binary: infracanvas (cmd/infracanvas/)

SubcommandPurpose
serveAll-in-one: relay + agent + embedded UI in one process
agentAgent-only, connects to external relay
discoverOne-shot discovery, prints JSON
get / exportQuery running instance
logsStream container logs
url / diagnoseOperational helpers

Serve mode flow

  1. Bind HTTP on 127.0.0.1:7777 (with tunnel) or 0.0.0.0:7777 (no-tunnel)
  2. Start Cloudflare quick-tunnel → print public URL with ?token=<ui_token>
  3. Start in-process agent connecting to ws://127.0.0.1:7777/ws/agent
  4. Browser auto-pairs (local mode, no pair code needed)

OSS Relay Server (pkg/server/)

Wire protocol — all messages use {"type": "...", "data": {...}} JSON envelope.

DirectionMessageMeaning
Agent → ServerHELLOhostname + scope
Agent → ServerGRAPH_SNAPSHOTfull node/edge graph
Agent → ServerGRAPH_DIFFincremental changes
Agent → ServerACTION_RESULTaction completed
Agent → ServerHEARTBEATkeepalive
Server → AgentPAIR_CODEassigned pair code
Server → AgentACTION_REQUESTbrowser-triggered action
Browser → ServerPAIRconnect using pair code
Browser → ServerBROWSER_ACTIONtrigger action on agent
Server → BrowserAGENT_CONNECTEDagent metadata
Server → BrowserGRAPH_SNAPSHOT / GRAPH_DIFFrelayed from agent

Session lifecycle:

  • Agent connects → server creates Session (UUID + pair code e.g. DUSK-DEER-755033)
  • Browser sends PAIR {code} → bound to session
  • Last snapshot cached for late-joining browsers
  • Local mode: browser auto-binds, no pair code needed

Agent (pkg/agent/)

  • WSAgent maintains WebSocket connection to relay
  • On connect: sends HELLO, gets PAIR_CODE
  • Every RefreshSeconds: runs orchestrator → computes diff vs previous snapshot → sends GRAPH_DIFF
  • On first run or reconnect: sends full GRAPH_SNAPSHOT
  • Handles ACTION_REQUEST from browser (docker stop/start, k8s ops, exec sessions, log streaming)

Discovery (pkg/discovery/)

Three scopes, discovered independently:

ScopeSourceKey data
host/proc, sysinfoCPU, memory, disk, network interfaces
dockerDocker socketcontainers, images, networks, volumes
kubernetes~/.kube/config or in-clusterpods, deployments, services, namespaces

Output: GraphOutput {nodes: GraphNode[], edges: GraphEdge[]} from pkg/output/

Actions (pkg/actions/)

Triggered by browser via BROWSER_ACTION → relay → ACTION_REQUEST on agent:

  • docker: start/stop/restart container, pull image, exec into container
  • host: exec shell command, PTY terminal session
  • kubernetes: scale deployment, update image, rollout restart

Exec sessions: agent opens PTY or Docker exec, streams stdin/stdout via EXEC_DATA messages.


SaaS Stack

Backend: saas-backend/ (binary: infracanvas-api, port 8090)

Packages:

  • pkg/api/auth/ — JWT auth, registration, login, forgot-password
  • pkg/api/agents/ — agent registry, list agents per org
  • pkg/api/websocket/ — SaaS relay (agent ↔ browser via org/agent ID)
  • pkg/api/organizations/ — org management, API keys
  • pkg/api/billing/ — billing integration
  • pkg/middleware/ — JWT validation, org context
  • pkg/database/ — Postgres connection (pg driver)
  • pkg/models/ — User, Org, Agent, APIKey structs

Auth flow:

  • Register/login → JWT
  • API keys for agent authentication (/api/v1/org/api-keys)
  • Agent connects with API key → registered under org
  • Browser connects with JWT → sees agents for their org

SaaS relay flow (different from OSS):

  • Agent authenticates with org API key
  • [relay/agent] HELLO agent=ByteStrix-5bcfabe8 org=bytestrix-657a9cc0
  • Browser connects → sees all agents in org
  • Relay bridges browser ↔ specific agent by agent ID (not pair code)

SaaS Frontend (~/infracanvas-saas-frontend/ on bytestrix — separate repo from OSS)

Next.js 14 App Router SPA. Deployed as dev server on :3001.

Key routes (frontend/app/):

  • / → redirect to /dashboard
  • /auth/login, /auth/register, /auth/forgot-password
  • /dashboard — main canvas page

State management (frontend/store/vmStore.ts):

  • Zustand store: vms: Record<code, VMState>
  • VMState: {status, hostname, scope, graph, error, lastUpdated}
  • applyVMDiff() — merges incremental diffs into graph state

WebSocket client (frontend/lib/wsManager.ts):

  • Module-level singleton (survives React navigation)
  • Exponential backoff reconnect (1s → 30s, ±20% jitter)
  • Derives WS URL from window.location (same-origin /ws/canvas)
  • OSS mode: sends PAIR {code: "local"} for auto-pair
  • SaaS mode: sends auth token, then selects agent by ID

Canvas rendering (frontend/components/canvas/):

  • React Flow for graph layout
  • frontend/lib/graphPreprocess.ts — transforms GraphOutput → React Flow nodes/edges
  • frontend/lib/layout.ts — hierarchical layout algorithm

Infrastructure on bytestrix VM (13.200.198.166)

Processes

ProcessPortBinary
infracanvas-server (Docker)8080OSS relay server
infracanvas-api (systemd)8090SaaS backend
Next.js dev server3001SaaS frontend
Postgres5432DB for SaaS
Redis6379Sessions
nginx3000Proxy → 3001 + 8090

Nginx routing (port 3000 — working dev URL)

/ws/   → localhost:8090  (SaaS relay WebSocket)
/api/  → localhost:8090  (SaaS REST API)
/      → localhost:3001  (Next.js frontend)

Port 80 is hijacked by k8s iptables DNAT → do not use port 80 for InfraCanvas.

Dev URL

http://13.200.198.166:3000

Agent connection (SaaS)

Agent on bytestrix connects to localhost:8090 (same VM). Registered as ByteStrix-5bcfabe8 under org bytestrix-657a9cc0. Install script downloaded from /api/v1/install/binary/linux-amd64.


Key Data Models

GraphNode

{
  id: string          // e.g. "/container:abc123"
  type: string        // "host" | "container" | "pod" | "service" | ...
  label: string
  layer: string       // "host" | "docker" | "kubernetes"
  metadata: Record<string, any>
}

GraphEdge

{
  id: string
  source: string      // node id
  target: string      // node id
  type: string        // "runs" | "connects" | "exposes" | ...
}

GraphDiff

{
  timestamp: string
  addedNodes: GraphNode[]
  modifiedNodes: GraphNode[]
  removedNodeIds: string[]
  addedEdges: GraphEdge[]
  removedEdgeIds: string[]
}

OSS vs SaaS Comparison

OSS (infracanvas serve)SaaS
AuthUI token (random hex)JWT + API keys
Multi-VMNo (one relay = one VM)Yes (org → many agents)
PairingPair code or auto (local)Agent ID selection
HostingUser's own machinebytestrix VM
TunnelCloudflare quick-tunnelDirect (nginx proxy)
DBNonePostgres + Redis

Development Workflow

# Local dev (OSS) — run everything in one command
./bin/infracanvas serve

# SaaS frontend dev (separate repo on bytestrix)
# ssh bytestrix, then:
cd ~/infracanvas-saas-frontend && npm run dev -- --port 3001
# OR managed by systemd:
sudo systemctl restart infracanvas-frontend

# SaaS backend dev (on bytestrix)
sudo systemctl restart infracanvas-api
sudo journalctl -u infracanvas-api -f

# Check agent connected
curl http://localhost:8090/api/v1/agents -H "Authorization: Bearer <key>"