Skip to main content

Proxy Networks (BungeeCord / Waterfall / Velocity)

ResourcePackManager runs on proxy networks. There is one jarResourcePackManager.jar — and you install the same jar on every backend and on the proxy. On a backend it runs in backend role (merge, host, push, convert); on a Velocity or BungeeCord/Waterfall proxy the same jar auto-detects the platform loader and runs in proxy role (network-side Bedrock delivery). The two roles link themselves: the proxy owns the network key and pushes it to each backend the first time a player connects there — no separate per-platform proxy jar, no key to paste.

Throughout this page "the proxy plugin" is shorthand for that same ResourcePackManager.jar running in proxy role.

What the Proxy Plugin Does

The proxy plugin's job is Bedrock pack merging and delivery. On a network with several backends, each backend produces its own Bedrock pack (the converted version of its merged Java pack). The proxy plugin:

  1. Polls each backend's small HTTP server every 5 seconds for /bedrock.zip and /mappings.json, preferring strong ETags with If-None-Match and retaining If-Modified-Since compatibility for older backends. Unchanged files return 304, so they cost roughly zero bandwidth. Each backend announces the exact HTTP port it bound, so the proxy normally hits the right port automatically (see Backend HTTP port resolution).
  2. Waits for the inbox state to stabilize — it merges as soon as two adjacent polls observe the same set of file hashes.
  3. Merges every backend's Bedrock pack into a single network-wide pack.
  4. When item mappings exist, copies the merged Geyser custom-mappings file into plugins/Geyser-*/custom_mappings/ on the proxy.
  5. Serves the merged pack to Bedrock clients on connect via Geyser's SessionLoadResourcePacksEvent.

The proxy plugin is only useful for Bedrock players. Java pack delivery on networks still happens per-backend through the regular setResourcePack / addResourcePack API — Java clients see whichever backend's pack the backend they're on chose to send. If your network is Java-only, you don't need the proxy plugin.

Setup — One Step (Plus Floodgate, If You Want Bedrock)

Floodgate

Floodgate is required for Bedrock players to authenticate on the proxy. It is not required for RSPM to link the proxy to its backends — a Java-only network can run the proxy plugin without Floodgate at all.

If Floodgate is installed on the proxy at the moment RSPM first boots there, RSPM seeds its network key from plugins/floodgate/key.pem once, so an existing network that already worked keeps the same identity across the upgrade. After that first boot the key lives in RSPM's own network-key file and key.pem is never read again.

Copy ResourcePackManager.jar to the proxy

There is only one jar. The same ResourcePackManager.jar you install on Bukkit/Paper backends is also the proxy plugin — it ships the Velocity and BungeeCord/Waterfall implementations together in one shaded jar and auto-detects which platform it's running on. There is no separate per-platform proxy jar to extract.

If you already started the backends without putting RSPM on the proxy, you don't have to go hunting for the download: each proxied, unkeyed backend stages a byte-identical copy of its own jar at plugins/ResourcePackManager/proxy-extension/ResourcePackManager.jar and prints that exact path in the console. Copying that file guarantees proxy and backend can never be on different versions.

Copy the same ResourcePackManager.jar into your proxy's plugins/ folder:

Proxy softwareUse this jar
VelocityResourcePackManager.jar
BungeeCordResourcePackManager.jar
Waterfall (Bungee fork)ResourcePackManager.jar

You can confirm this from a backend at any time by running /rspm status; the Proxy deployment section prints Network proxy jar: ResourcePackManager.jar and Use the same jar on Bukkit/Paper, Velocity, and BungeeCord/Waterfall.

Restart the proxy. That's it.

There is no required config to edit and no key to paste. The proxy establishes the network key itself at boot and provisions each backend with it automatically — see How the proxy and backends link below. The generated proxy config.yml contains only two optional settings, both described further down.

The proxy's first poll fires ~2 seconds after boot, then every 5 seconds. Combined with the single-cycle stability gate, the first merge is published roughly 7 seconds after the proxy can see at least one backend that's already producing content.

How the Proxy and Backends Link

Everything that follows happens without configuration. It is documented here because when a network doesn't link, knowing the sequence is the whole diagnosis.

1. The proxy establishes the key. On boot it resolves exactly one value, in this order:

  1. its saved network-key file, in the proxy plugin's own data folder — the steady state on every boot after the first;
  2. a one-time seed derived from plugins/floodgate/key.pem on the proxy, if that file exists;
  3. a freshly minted random key.

Whichever branch runs, the result is written to network-key and reused forever after. The proxy console says which happened (Network key loaded ✓, adopted from Floodgate key.pem, or New network key generated). If the file cannot be written, the proxy logs an error at [ERROR] level — that state is not fatal for the current boot, but the next restart mints a different key and silently unlinks every backend, so fix the folder permissions.

2. The proxy pushes the key to each backend. As soon as a player connects to a backend, the proxy sends the key to it over the rspm:network plugin channel. The direction is deliberate: a backend physically cannot ask, because a proxy will not forward a plugin message on a channel the player's own client never registered.

3. The backend adopts and persists it. It saves the key to its data.yml and reports Network key received from the proxy; this backend is now linked. Grants are honoured only while the backend holds no key — an established backend logs a warning and keeps what it has rather than letting a message re-point it at another network.

On Velocity with modern forwarding, grants are signed with an HMAC over the proxy's forwarding.secret, and a backend configured for modern forwarding requires that signature. On BungeeCord, Waterfall, and legacy forwarding there is no shared secret to sign with, so grants are unsigned and accepted as such.

A backend that reaches step 2 but never step 3 keeps its key source at not set. That is the exact state that triggers the proxy-extension/ staging and the Bedrock-specific warning banner.

Verifying It Works

Proxy console

Within ~10 seconds of restart (assuming backends are running) you should see:

[ResourcePackManager] Network key loaded ✓
[ResourcePackManager] Network key provisioning ready on rspm:network; backends are keyed as players connect.
[ResourcePackManager] NetworkSync starting (poll interval 5000 ms, network-http-offset 1 - endpoint announcements preferred, fallback HTTP port = mcPort + offset)
[ResourcePackManager] NetworkSync: inbox stabilized — merging 2 Bedrock zip(s) and 2 mappings file(s) across 2 backend(s).
[ResourcePackManager] Merged Bedrock pack published at .../merged/Bedrock.zip (sha1=...).
[ResourcePackManager] ✔ Network resource pack is now ready (... KB, sha1 ABCD1234)

/rspm status on the proxy

Prints a snapshot: network key fingerprint, backend list, per-backend per-path fetch outcomes (200 / 304 / 404 / CONNECT_FAILED) for /bedrock.zip, /mappings.json and /rspm-update.jar, the endpoint announcements it has received, the relay entries it can see, the consecutive-empty-poll counter, whether the merged Bedrock pack and merged mappings are on disk (with their sizes) plus the current merged pack's SHA-1 prefix, the network-http-offset fallback, the detected Geyser plugin folder and deployed mappings file, and Floodgate / Geyser presence on the proxy. Velocity accepts resourcepackmanager.command.status or resourcepackmanager.*; BungeeCord registers the exact resourcepackmanager.command.status node, which permission plugins may satisfy through wildcard expansion. Console can run it. The output reveals no secrets (the network key appears only as a short one-way hash fingerprint, and no auth tokens are printed), so liberal grants are safe.

/rspm status on a backend

The Deploy mode line should read network-backend. Network key fingerprint should show a short hash — it must match the fingerprint the proxy prints. Network key source tells you how this backend got it:

Source lineMeaning
provided by the proxyThe normal linked state. The proxy granted the key on a player connect.
saved on this serverAlready keyed on a previous boot (or a standalone server that minted its own).
adopted from Floodgate's keySeeded once from this backend's local plugins/floodgate/key.pem.
not set — the proxy sends one when a player next connects hereNot linked. Either no player has connected since the proxy came up, or the proxy is missing RSPM / is an unsupported proxy.

Bedrock client

Connect via Bedrock. You should see the resource-pack download prompt before reaching the world. Custom items render with their intended models instead of plain armor stands.

Configuration Reference

Backend plugins/ResourcePackManager/config.yml

Network mode is auto-detected — there's no networkMode: true flag to set. Detection signals (any one is sufficient):

  1. Floodgate present, Geyser-Spigot absent — strongest signal for the Bedrock-via-proxy case.
  2. spigot.yml: settings.bungeecord: true — legacy BungeeCord / Waterfall IP-forwarding switch.
  3. paper-global.yml: proxies.velocity.enabled: true — modern Velocity forwarding.

The only knob that matters specifically for network mode is networkHttpOffset-v2, which controls the fallback HTTP port the proxy will poll on each backend (the port the proxy guesses before a backend has announced the port it actually bound). Default 1 works on virtually all hosting. See Self-hosting for the full port-resolution story.

In normal operation each backend announces its real HTTP port to the proxy automatically (via the magmaguy.com endpoint registry), so the offset is only consulted as a startup/failure fallback. See Backend HTTP port resolution below.

Proxy config.yml

The proxy plugin writes a minimal default config on first boot. The folder differs by platform, because each proxy derives it from its own plugin identifier:

Proxy softwareConfig path
Velocityplugins/resourcepackmanager/config.yml
BungeeCord / Waterfallplugins/ResourcePackManager/config.yml

It contains exactly two settings, both optional:

# FALLBACK offset added to each backend's Minecraft port to derive the HTTP
# port this proxy will hit for /bedrock.zip and /mappings.json BEFORE that
# backend has announced its real ResourcePackManager HTTP port. Default 1.
# In normal operation the backend announces the exact port it bound, so this
# value is only used at startup or if announcement fails — but if it IS used,
# it should match each backend's networkHttpOffset-v2.
network-http-offset-v2: 1

# Installs and updates this universal ResourcePackManager.jar in the proxy's
# Geyser extensions folder, which is what makes custom Bedrock ENTITIES render.
# Default true. Setting it to false stops future installation and update
# staging; it does not delete an extension jar that is already installed —
# remove that by hand while Geyser is stopped.
geyser-extension-auto-install: true

The backend equivalents are networkHttpOffset-v2 and geyserExtensionAutoInstall in the backend config.yml. The two sides are separate settings — turning the extension install off on the proxy does not turn it off on the backends, and vice versa.

Alongside config.yml the proxy plugin also writes a network-key file in the same folder. That is generated state, not configuration: do not edit it, and do not copy it between different networks. Deleting it makes the proxy mint a brand-new key on its next boot, which unlinks every backend that was already provisioned (each backend keeps the old key and will not adopt a replacement). Copying it is the correct move in one case only: two proxies fronting the same set of backends, which must share one key.

There is no proxy-side force-resource-pack setting. Forcing pack acceptance is a backend-side decision (forceResourcePack in the backend config.yml) because the proxy plugin only handles Bedrock pack delivery — it never sends Java packs. If an older proxy config.yml still carries a force-resource-pack line, it is simply ignored and can be deleted.

There is intentionally no network-key config entry — a pasteable key was retired pre-release because typos silently broke the proxy↔backend link with no error anywhere. The key is established by the proxy and provisioned to backends automatically, as described in How the proxy and backends link.

Backend HTTP Port Resolution

The proxy needs to know which HTTP port to poll on each backend for /bedrock.zip and /mappings.json. It resolves that port in this order:

  1. Backend-announced endpoint (preferred). Each backend uploads the exact HTTP port it bound to the magmaguy.com endpoint registry, keyed by the network key. On each poll the proxy refreshes this list and matches a backend's announced port to the server-list entry by Minecraft port (and host when available). This means an admin who sets an explicit selfHostPort on a backend, or whose backend lands on mcPort + 1 automatically, is handled identically — the proxy uses whatever the backend actually bound.
  2. mcPort + network-http-offset-v2 (fallback). Used only when no matching announcement is available (e.g. the first poll before any backend has announced, or the endpoint registry is briefly unreachable). This is why the two offsets should still agree if you rely on the fallback.

The proxy deliberately does not port-scan a backend as a fallback — that looks like abusive behavior to the host. When direct fetch can't work at all, the relay path (below) is the categorical answer.

/rspm status on the proxy shows, per backend, which port was chosen and whether it came from an announcement or the offset fallback.

Stability and Merge Cadence

The proxy waits for the inbox state to stabilize before merging: the first poll sets the baseline file-hash set, and the next poll that observes the same set triggers the merge (a one-cycle stability gate). With the ~2 s initial delay and 5 s poll interval that puts the first merge ~7 s after the proxy can see at least one backend producing content. Subsequent merges only re-zip when the SHA-1 set of inbox files changes, so a long quiet period costs roughly nothing.

Backends write their bedrock.zip to a temp file and atomically rename it, so the /bedrock.zip route always serves a complete zip — the proxy never has to defend against half-written reads, which is why the gate is a single cycle rather than two.

Direct Fetch vs Relay Fallback

The default path is direct fetch: the proxy makes an HTTP GET to http://<backend-host>:<mcPort + offset><path> for each backend.

If the proxy can't reach a backend's HTTP port directly (typical of shared / managed Minecraft hosting where MC ports are exposed but adjacent ports are firewalled), the backend pushes its bedrock.zip and mappings.json to a relay endpoint on magmaguy.com under the network's namespace (derived from the network key). The proxy lists and downloads from the relay when direct fetch fails hard.

Both paths feed into the same merge step, so the operator never has to choose — direct fetch is preferred (zero bandwidth cost to magmaguy.com), relay kicks in transparently when needed.

The relay has a 30-minute TTL on the server side. Backends push every 25 minutes to keep the entry alive; clean shutdown drops the entry immediately rather than waiting for TTL.

Troubleshooting Common Issues

A backend never gets a network key

Symptom: /rspm status on the backend shows Network key source: not set — the proxy sends one when a player next connects here, and the backend console repeats:

[ResourcePackManager] This backend is behind a proxy but has no network key yet, so it is not linked to the proxy.
[ResourcePackManager] The proxy sends one automatically the first time a player connects to this server.

Work through these in order:

  1. Has anyone actually connected to that backend since the proxy started? The grant rides on a player connection. A lobby nobody has visited yet is legitimately unkeyed.
  2. Is RSPM on the proxy at all? The backend stages a copy of its own jar at plugins/ResourcePackManager/proxy-extension/ResourcePackManager.jar for exactly this case. Copy it into the proxy's plugins/ folder and restart the proxy.
  3. Velocity modern forwarding: does the backend's forwarding secret match the proxy's? A backend configured for modern forwarding rejects an unsigned or wrongly-signed grant on purpose — that check is the only thing preventing an unauthorized proxy from keying your backends. The backend log names which of the two cases it hit. Fix the secret on both sides and the next player connect retries automatically; no restart of either side is needed to re-arm.
  4. Is the proxy an unsupported proxy? Only Velocity and BungeeCord/Waterfall run the proxy role.

Note that Floodgate's key.pem is no longer part of this. A missing plugins/floodgate/key.pem on the proxy is a fully supported state — the proxy mints its own key instead. Floodgate is still required for Bedrock players to reach the proxy at all.

Two proxies front the same backends

Both proxies must present the same network key, or each backend links to whichever one reached it first and rejects the other (logging A proxy offered a network key that differs from the one this backend already uses). Copy the network-key file from the primary proxy's RSPM data folder to the other proxy's, and restart it.

"No merged pack content" warning after ~20 seconds

After 4 consecutive empty poll cycles, the proxy logs a one-shot multi-line warning listing every backend it polled, the URL it tried, and the outcome. The warning explains the most common fixes:

  • CONNECT_FAILED on every backend — the proxy can't reach the backend HTTP port at all. Check that the address in velocity.toml / config.yml is one the proxy can actually reach (not a Docker-internal name that doesn't resolve from the proxy's network), and that the backend's HTTP port is open between proxy and backend. The warning prints the exact HTTP host:port it tried for each backend and whether that port came from the backend's announcement or the mcPort + networkHttpOffset-v2 fallback. If you have no way to open that port (managed hosting), see the relay fallback section above — the backend should be uploading to the relay automatically.
  • NOT_FOUND_404 on every backend — backends are up but not producing a Bedrock pack. Run /rspm status on each backend; the Bedrock Pack diagnostic block will tell you why (most often: no convertible item mappings or entity bundles in the merged pack, or the first mix cycle hasn't completed yet).

The warning fires once per stuck period. An NetworkSync: recovered line is logged when at least one backend starts returning content again.

Bedrock players see no custom models on first proxy boot

Geyser registers its custom-item table at proxy startup only. If the proxy started before any backend produced a Bedrock pack, Geyser is running with an empty mappings table and stays that way for the rest of the session.

Fix: restart the proxy once after the backend has logged its first Merged Bedrock pack published line. RSPM pre-deploys the previous run's mappings on every proxy boot, so this only ever bites on a brand-new install — subsequent boots have something ready before Geyser scans.

"Duplicate bedrock_identifier" warnings on proxy boot

Two backends emitted the same Bedrock identifier for the same base item. Last-writer-wins; harmless if you only need one backend to provide that item. If both backends should host distinct custom items under the same base item, rename one of the source Java models so the auto-generated hashes differ.

Bedrock player connected but doesn't see the pack

The proxy fires a chat banner to all online Java players when a Bedrock session loads without a usable RSPM pack:

⚠ [RSPM] Bedrock player Alice connected before the resource pack was ready — they're seeing plain armor stands instead of custom models. Tell them to disconnect and reconnect; the pack will load on their next session. (Cause: ...)

The Bedrock player themselves also gets a modal popup, and the proxy console gets a banner. If you need to dig deeper, enable the debug stream (available on both Velocity and BungeeCord):

/rspm debug bedrock on

That turns on verbose [RSPM-BedrockDebug] log lines from GeyserBinder. Reproduce the issue, then turn it off:

/rspm debug bedrock off

The setting resets to off on proxy restart so it can't accidentally be left on.

Updating RSPM on a Network

It's the same jar on every backend and on the proxy, so keep all components on the same version.

The manual route is: bump the backend jar, re-copy that same ResourcePackManager.jar to the proxy's plugins/ folder, restart the proxy.

RSPM can also do the proxy half itself. Each backend offers its universal plugin jar to the proxy over an authenticated route (/rspm-update.jar, protected by a token derived from the shared network key — the key itself is never transmitted). The proxy validates that the offered file is a complete universal RSPM jar with matching platform descriptor versions and expected entry points, refuses downgrades, and verifies the staged bytes against the advertised size and SHA-256 before replacing anything. It stages the same jar for the proxy plugin and, when proxy-hosted Geyser is detected, its bundled Geyser extension; it applies those files on proxy shutdown and keeps the previous files as rollback copies.

This is an authenticated network trust boundary, not an independent provenance check against Nightbreak: any backend that has been provisioned with the shared network key is trusted to offer a structurally valid same-or-newer RSPM jar. Protect that key and treat every backend sharing it as trusted infrastructure.

So in practice, updating the backends and then restarting the proxy is usually enough — the proxy will have staged the matching jar for itself. A rejected update logs Rejected backend-offered ResourcePackManager update: ... with the reason.

What's Not Yet Supported

  • Java packs on networks via the proxy plugin. Java clients receive packs from each backend directly through the regular API. There is no proxy-side Java-pack mixer.
  • Cross-backend Java pack merging. Each backend's Java pack is independent. If a player switches backends, they receive the new backend's pack.
  • Live rotation of the network key. A backend adopts a key exactly once and will not accept a replacement from a message. Rotating means deleting the proxy's network-key file and clearing networkKey from every backend's data.yml, then restarting everything.