CommandBridge Logo About Docs
GitHub Discord Download

Multi Proxy

CB supports setups with more than one Velocity proxy. I already talked about this in the Configuration overview, but here is the full picture.

The idea is simple: only one Velocity runs in server mode. Every other Velocity instance runs as a client. That is what the act-as-client value in the config is for. The client-mode proxies connect to the main one and behave like any other backend: they authenticate, receive command registrations, and execute commands. The only difference is that CB internally marks them as VELOCITY instead of BACKEND, so it knows they are proxies and not actual game servers.

%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '18px', 'clusterBkg': 'transparent', 'clusterBorder': 'transparent', 'edgeLabelBackground': 'transparent'}, 'flowchart': {'padding': 20, 'nodeSpacing': 40, 'rankSpacing': 60}} }%%
flowchart TB
    subgraph primary ["Primary Velocity"]
        V["CommandBridge"]
    end

    subgraph clients ["Client-mode proxies"]
        V2["Velocity 2<br/>act-as-client: true"]
        V3["Velocity 3<br/>act-as-client: true"]
    end

    subgraph backends ["Backend servers"]
        B1["survival-1"]
        B2["creative-1"]
        B3["lobby-1"]
    end

    V2 -- "connects as client" --> V
    V3 -- "connects as client" --> V
    B1 -- "connects as client" --> V
    B2 -- "connects as client" --> V
    B3 -- "connects as client" --> V

    classDef primary fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
    classDef client fill:#3b1f5e,stroke:#a855f7,color:#d8b4fe
    classDef backend fill:#14532d,stroke:#22c55e,color:#86efac
    class V primary
    class V2,V3 client
    class B1,B2,B3 backend

So there is no mesh or anything fancy. One proxy manages everything, the rest just connect to it.

Only one Velocity instance can run in server mode. If you start two proxies in server mode on the same Redis instance or the same WebSocket port, things will break.


act-as-client

On every Velocity that should not be the main one, open plugins/commandbridge/config.yml and set:

act-as-client: true

That is the only thing you change in config.yml. When this is enabled, CB ignores everything else in that file; endpoints, security, timeouts, all of it. Instead it reads the connection config from a separate file called client.yml.


client.yml

When act-as-client is true, CB creates a client.yml in plugins/commandbridge/ on first startup. This file looks exactly like a backend config, because that is basically what this proxy becomes:

client-id: proxy-2
endpoint-type: WEBSOCKET
endpoints:
  websocket:
    host: 127.0.0.1
    port: 8765
  redis:
    host: 127.0.0.1
    port: 6379
    username: ''
    password: ''
security:
  tls-mode: TOFU
  tls-pin: ''
  secret: change-me
timeouts:
  auth-timeout: 5
  reconnect-timeout: 60
  reconnect-interval: 5
debug: false

If you already configured a backend before, this should look familiar. It is the same schema as the Backends config. The client-id is the unique name for this proxy, endpoints point to wherever the primary proxy is reachable, and security needs the same secret as everything else. I will not explain every field again here since it is identical to the backend config.

The config.yml is still read on startup to check the act-as-client flag. But when it is true, everything else in config.yml is skipped and client.yml takes over completely.


Registering & executing across proxies

In your scripts, you can target more than one instance for both registration and execution. So if you have three Velocity proxies, you can register a command on all of them:

register:
  - id: "proxy-1"
    location: VELOCITY
  - id: "proxy-2"
    location: VELOCITY
  - id: "proxy-3"
    location: VELOCITY

You can mix proxies and backends too:

register:
  - id: "proxy-1"
    location: VELOCITY
  - id: "survival-1"
    location: BACKEND

The location tells CB what type of target it is: VELOCITY for a proxy (primary or client-mode), BACKEND for a game server.

The execute section works the same way. You can send a command to client-mode proxies, backends, or both:


Player presence tracking

CB tracks which players are on which server in real time. Every client, both backends and client-mode proxies, reports player changes to the primary. The primary stores all of this in a tracker that maps each client ID to a set of player UUIDs.

The initial sync on auth is a full snapshot so both sides start from the same state. After that, joins and leaves are sent individually.

%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '18px', 'clusterBkg': 'transparent', 'clusterBorder': 'transparent', 'edgeLabelBackground': 'transparent'}, 'flowchart': {'padding': 20, 'nodeSpacing': 40, 'rankSpacing': 60}} }%%
flowchart TB
    T{Type?}
    T -- "Auth" --> SNAP["Send PLAYER_LIST<br>(full snapshot)"]
    SNAP --> STORE["Store full set"]:::success
    T -- "Join" --> PJ["Send PLAYER_JOIN"]
    PJ --> ADD["Add UUID to set"]:::success
    T -- "Leave" --> DELAY["Wait 1 tick / 50ms"]
    DELAY --> PL["Send PLAYER_LEAVE"]
    PL --> REM["Remove UUID<br>from set"]:::success
    T -- "Disconnect" --> WIPE["Wipe client's<br>player set"]:::reject

    classDef success fill:#14532d,stroke:#22c55e,color:#86efac
    classDef reject fill:#7f1d1d,stroke:#ef4444,color:#fca5a5

The delay on leave is there so the player is actually gone from the server's player list before the message is sent.


target-required

The target-required setting controls whether a command should only execute on a target if the player is actually on that server. This is where the player tracking from above comes in.

server:
  target-required: true

When this is enabled, CB checks the player tracker before dispatching to each target. If the player is not on that target, the command is skipped for it.

This works for both BACKEND and VELOCITY targets. Since every client, whether it is a Paper backend or a client-mode Velocity, sends its player data to the primary, CB has full visibility across the entire network. It does not matter which proxy the player originally connected through. If a backend reports that uuid-123 is online, the primary knows about it.

For the primary Velocity itself there is a small shortcut: CB does not need to check the tracker because the primary already knows its own players directly. But for every remote target the tracker is used, regardless of whether it is a backend or another proxy. So if you have target-required: true and the player is on survival-1, the command runs on survival-1 and gets skipped on creative-1.