Skip to content

Client Lifecycle ​

This page describes the dynamic node lifecycle: download, install, bind, online state, disconnects, reset, and control page transitions. Static nodes do not expose bind commands or client control.

Control Page Entry ​

The node control page depends on client_initialized:

StatePage
client_initialized=falseClient initialization page: download, install, bind.
client_initialized=trueControl page: status, logs, config push, tunnel start/stop.

client_initialized=true only means a client has completed binding before. It does not mean the client is currently online. Online state is projected by the backend from MQTT signals.

Initialization Steps ​

The initialization page has three steps:

  1. Download the client.
  2. Install the client service.
  3. Generate and copy the node bind command.

After install, the user runs the bind command in a terminal. After binding, the client writes a local profile and connects to MQTT.

Bind Command Generation ​

mermaid
sequenceDiagram
    participant U as "Admin"
    participant F as "Console"
    participant B as "Backend"

    U->>F: Generate bind command
    F->>B: POST /api/v1/configs/{config_id}/nodes/{node_id}/bind-command
    B->>B: Validate dynamic node
    B->>B: Create 5-minute one-time bind token
    B-->>F: Return command and expires_at
    F-->>U: Copy command

Rules:

  • Bind tokens expire after 5 minutes by default.
  • A bind token can be used successfully only once.
  • Changing a node to static, resetting the client, or regenerating the token invalidates old tokens.

Successful Bind ​

mermaid
sequenceDiagram
    participant T as "Terminal"
    participant C as "wfmctl"
    participant B as "Backend"
    participant E as "EMQX"
    participant A as "wfm-agent"

    T->>C: wfmctl bind --server --token
    C->>B: POST /api/client/bind
    B->>B: Validate token and dynamic node
    B->>B: Generate or rotate MQTT credentials
    B->>B: Mark client_initialized=true
    B->>E: Create or update MQTT user
    Note over B,E: EMQX failure does not roll back database state
    B-->>C: Return profile, mqtt, topics
    C->>C: Write local profile
    C->>A: Restart service if running
    A->>E: Connect with node credentials
    A->>E: Send heartbeat periodically
    E->>B: Forward heartbeat
    B->>B: Update runtime state

The database remains the source of truth. If EMQX is temporarily unavailable, binding still writes database state; synchronization reconciles EMQX later.

Page Switch After Online Signal ​

mermaid
sequenceDiagram
    participant A as "wfm-agent"
    participant E as "EMQX"
    participant B as "Backend"
    participant S as "SSE"
    participant F as "Console"

    A->>E: Publish heartbeat / ACK / event
    E->>B: Forward upstream message
    B->>B: Write runtime state
    B->>S: endpoint.status.updated
    S->>F: Page receives update
    F->>F: Switch from initialization to control page

The frontend should not guess this transition. It follows backend state and SSE events.

Unexpected Disconnect ​

mermaid
sequenceDiagram
    participant A as "wfm-agent"
    participant E as "EMQX"
    participant B as "Backend"
    participant S as "SSE"
    participant F as "Console"

    A--xE: Unexpected disconnect
    B->>B: Heartbeat timeout or detect failure
    B->>B: Mark client_presence_state=dropped
    B->>S: endpoint.status.updated
    S->>F: Show dropped state

Unexpected disconnects do not return the page to initialization. The node is still initialized; it is only currently unreachable.

Reset Client ​

mermaid
sequenceDiagram
    participant U as "Admin"
    participant F as "Console"
    participant B as "Backend"
    participant E as "EMQX"
    participant S as "SSE"

    U->>F: Reset client
    F->>B: POST /api/v1/configs/{config_id}/nodes/{node_id}/reset-client
    B->>B: Clear runtime state
    B->>B: client_initialized=false
    B->>B: client_presence_state=offline
    B->>E: Delete or disable MQTT user and disconnect client
    Note over B,E: EMQX failure does not block reset
    B->>S: endpoint.status.updated
    S->>F: Return to initialization page

After reset, old MQTT credentials must not continue working. The client must bind again.

Dynamic to Static ​

When a dynamic node becomes static, the backend must:

  • Clear binding permissions.
  • Clear runtime state.
  • Set client_initialized=false.
  • Set client_presence_state=offline.
  • Delete or disable the EMQX user.
  • Publish an endpoint status event.

Static nodes do not show bind commands and do not support endpoint control.

Final State Semantics ​

StateRule
onlineAny recent reachable signal exists and no newer offline signal exists.
droppedAll reachable signals exceed TTL, or detect fails/times out without another recent signal.
offlineNever bound, will message received, client reset, node changed to static, or permission revoked.

Reachable signals include:

  • heartbeat
  • detect ACK
  • control ACK
  • info ACK
  • config push ACK
  • non-offline events

The client sends a heartbeat every 30 minutes. The server online TTL must be longer than that interval so a single missed heartbeat does not cause a false disconnect.

Version and Config Projection ​

Client version comes from bind requests and detect ACKs. The backend stores it in node_client_state, and the frontend only displays backend fields.

WG config version state is calculated by the backend:

  • latest: confirmed client state matches staged server state.
  • pending: config has not been pushed or confirmed state is behind.

The frontend displays this state and does not calculate it independently.

Diagnostics ​

When the user clicks "show WG information":

  1. Backend publishes wg_show to the info topic.
  2. Client runs raw wg or awg.
  3. info/ack only marks completion or failure.
  4. stdout/stderr is sent through event.
  5. Backend writes control logs and pushes SSE events.

Raw wg / awg output is diagnostic only. It can include all host interfaces and does not participate in current profile runtime projection.