Building bloodstrike backend · Lesson 4 of 4
Connecting a Vercel Frontend to a Secure WebSocket Backend
Configuring browser origins, WSS, and end-to-end connection tests
Real-time games need more than ordinary HTTP requests. The client and server must be able to exchange messages continuously, without opening a new HTTP request for every update. For Bloodstrike, we used WebSockets and placed the game server behind Caddy so that browser traffic could use a secure connection.
Why a game needs WebSockets
Imagine a player joining a room. The game may need to send updates about players, room state, or other real-time events.
With a normal HTTP request, the client sends a request and receives a response. WebSockets provide a persistent connection that can carry messages in both directions.
Our application used the ws package in the Node.js game-server project.
We checked that the dependency was installed in the repository.
cd ~/apps/cs-bloodstrike
npm ls ws
The output showed ws@8.22.0 under the game-server workspace.
This was enough to confirm that the project had the WebSocket library available. We did not need to install another WebSocket server on the VPS.
Understanding HTTPS and WSS
The frontend was hosted at:
https://bloodstrike.gworkspace.fr
The backend was hosted at:
https://bloodstrike-api.gworkspace.fr
For ordinary HTTP requests, the client uses HTTPS. For WebSocket connections, the secure scheme is wss://.
Our WebSocket endpoint was:
wss://bloodstrike-api.gworkspace.fr/ws
Caddy handled the public TLS connection and forwarded traffic to the Node.js server on 127.0.0.1:8080.
This means the browser did not need to connect directly to the local port. Caddy could proxy the WebSocket upgrade through the same public domain used for the HTTP API.
Allowing the correct frontend origin
Browsers include an Origin header when establishing a WebSocket connection from a web page. Our game server checks that the origin is allowed.
The backend configuration included an ALLOWED_ORIGINS environment variable.
For our frontend, the relevant value was:
ALLOWED_ORIGINS=https://bloodstrike.gworkspace.fr
If the application also needs local development, the list can contain additional trusted origins separated by commas.
An origin includes the scheme, hostname, and optional port. It does not include a URL path.
For example, these are different origins:
http://localhost:3000
https://bloodstrike.gworkspace.fr
The production frontend must match the origin configured by the server.
Origin validation is useful for browser-based protection, but it is not a replacement for authentication, authorization, or validation of incoming game messages. Non-browser clients can also attempt WebSocket connections, so the server must not rely on the Origin header as its only security measure.
Applying the environment change
After updating the allowed origin, we restarted the game-server service so that it would load the new environment.
sudo systemctl restart bloodstrike-game
sudo systemctl is-active bloodstrike-game
The service returned:
active
We then checked the health endpoint again.
curl -i https://bloodstrike-api.gworkspace.fr/health
It returned 200 OK.
This confirmed that the backend was still running after the configuration change.
Testing a secure WebSocket connection
A successful health check does not prove that a WebSocket connection works. We needed to test the actual /ws endpoint.
The ws package was already available in the project, so we used it as a client for a quick connection test.
cd ~/apps/cs-bloodstrike
node -e "const WebSocket=require('ws'); const ws=new WebSocket('wss://bloodstrike-api.gworkspace.fr/ws',{origin:'https://bloodstrike.gworkspace.fr'}); ws.on('open',()=>{console.log('WebSocket connected');ws.close()}); ws.on('error',e=>{console.error('WebSocket error:',e.message);process.exitCode=1}); setTimeout(()=>{console.log('Timed out');process.exit(1)},5000).unref()"
The test reported a successful WebSocket connection.
This confirmed that a client could reach the public WSS endpoint, pass the configured origin check, and establish a connection through Caddy.
It did not yet prove that every game feature worked correctly. It only verified the connection itself.
Testing from the real frontend
The next test should be performed in the actual browser application.
Open the Bloodstrike frontend and open the browser developer tools. In the Network panel, filter by WS, then try to create or join a room.
A successful WebSocket handshake commonly appears with status 101 Switching Protocols.
If the connection fails, check the browser console and network details. Common causes include an incorrect backend URL, an origin mismatch, a stopped game-server service, or a proxy configuration problem.
A connection can also succeed while game actions fail because of application-level logic. For that reason, the final test should include actual room creation, joining, and message exchange.
The final architecture
The connection path is now:
Next.js frontend on Vercel
https://bloodstrike.gworkspace.fr
|
| HTTPS requests and WSS connection
v
Caddy reverse proxy
bloodstrike-api.gworkspace.fr
|
| Local traffic
v
Node.js game server
127.0.0.1:8080
|
v
HTTP routes and WebSocket endpoint
/health /rooms /ws
The public client uses secure protocols, while the game server remains bound to the local interface.
The firewall allows public access to the proxy ports but does not need a public rule for port 8080.
Summary
We confirmed that the WebSocket library was installed, configured the frontend origin, restarted the game-server service, and successfully tested a WSS connection through Caddy.
The HTTP endpoints and secure WebSocket handshake were working. The remaining step was to test complete gameplay from the real frontend, including creating and joining rooms and exchanging game messages.
The key lesson is that deploying a real-time application involves several separate checks: the DNS must resolve, HTTPS must work, the proxy must support WebSockets, the server must accept the correct origin, and the game logic must work after the connection is established.