Serving and communicating between two frontend apps proxied under the same address locally
Today I am writing about how my single player/co-op online persistent-universe game in development is built out of three main code repositories and parts: the backend, the portal website, and the actual game's repository. I will focus more on the frontend as it had an interesting issue I solved between the two frontend "apps", the website and the game.
Why separate the website and game?
After long months of having refactoring syndrome, I realized I need to actually make a game. That meant that client-side, I was going to hack down some minimal code for authentication with Auth0. Knowing that I don't have time to spend on a flashy website, I decided to separate the game and website to two modules that can be developed independently. Once I reached a playable point in the game development, I could shape up that portal website a bit and first launch my pre-pre-alpha version minus 0.0.12 or whatever I call that for the world. That's where I'm headed, so feedback can start coming in from early players that are interested in the game.
Auth0 was mentioned. The portal is a React-based Next.js app that basically just gets an access token that the "game app" should use for authenticating against the server backend (and also for getting the number of the player's active games and their ids to show on the portal, etc). How would I transfer that access token to a different "web app"?
Do you link to the game app with the token in a GET param? Not recommended. Do you host both portal and game apps on the same domain and share a cookie? Also not recommended, same reason as localStorage is not safe. HTTPOnly cookie maybe? Turns out you can't even set them from JavaScript and it requires some server intervention🤢. So what did I do?
Not sure if 🤢 or 😎 but I decided to have a page in my Next.js app (where I "know" the token) that loads the game app in a full-viewport good old iframe and then postMessage to it with the token. Simple enough, right?
It looks a bit like this (game
is a ref to an iframe):
# in Next.js (beware of throw-away code)
useEffect(() => {
if (isAuthenticated) {
const enterGame = async () => {
const token = await getAccessTokenSilently();
game.current.src = "/game";
game.current.addEventListener("load", () => {
game.current.contentWindow.postMessage({ accessToken: token });
});
};
enterGame();
}
}, [isAuthenticated]);
And in the game side of things:
# Game's index.ts (beware of throw-away code)
window.addEventListener("message", (event: MessageEvent) => {
const { hostname } = new URL(event.origin);
const ALLOWED_HOSTS = process.env.ALLOWED_HOSTS?.split(",");
if (ALLOWED_HOSTS.includes(hostname)) {
const { accessToken } = event.data;
console.log("setting token!");
main(accessToken).catch(console.error);
} else {
console.error(
`Disallowed hostname is communicating with me via postMessage "${hostname}"`
);
}
});
The process.env
bit is from the parcel bundler I use currently for the game's code.
The next page is hosted under /play/
and the game build's folder is hosted under /game/
as can be seen in game.current.src
I set on the Next.js side. It's easy enough to do that with the DigitalOcean App Platform where I currently host these two static frontend sites, but on localhost, while developing, it's a different story.
Reverse proxy for development
While developing locally, I faced a CORS issue when trying to load the game's iframe inside my main app's page. It would be much easier if I could serve under the same localhost:3000
.
I could use nginx or more modern kool-kids proxies like traefik or whatnot. But they are too complex for a quick setup as so commonly needed when developing for the web. After evaluating some solutions that didn't work or worked in a non-friendly way, I decided to write my own reverse-proxy intended for local development only and came up with this: github.com/amireldor/dev-local-proxy.
At the time of writing, it's still not packaged for an easy npm install
but I use it locally for the purpose I mentioned above and it works great!
I got inspiration from github.com/FND/dprox but that didn't work well enough for me at some point.
You could also serve your backend or API server under the same address locally if that's cool for you, but in my case I'm serving two frontend apps which is a bit different. My backend is configured to work with them with the proper CORS headers.
That would be it for today, may you have a funny weekend!