September 22, 2022

Bundles of Joy: Javascript Reverse Proxies for a Better Serverless World

Using a JavaScript reverse proxy in Cloudflare Workers to serve two static bundles from the same domain and make our marketing team happier in the process.

Column’s frontend code is built as a single page React application, written in the same mono-repo as the rest of our core backend. Historically, maintaining all of our code in the same GitHub repository has been a massive unlock on our team’s productivity. We take the concept of innovation tokens seriously. And right now, we do not view maintaining micro-service architectures as part of our core competencies.

Previously, our marketing site was a part of the same static bundle as the rest of our application. As a result, changes were tightly coupled to our release cycle. If our marketing team wanted to make a change, they either needed to operate on the same two-week cadence as our regular sprint cycles or ask for a hotfix that circumvented our normal release process. Unsurprisingly, our marketing team was dramatically slowed down by the pace at which they could make changes on their own, for even small copy adjustments. Also, because we do not use server-side rendering in our core bundle, we were taking a significant SEO hit.

Earlier this month Column announced its Series A and rebrand, all along with the release of a fresh marketing site. About six month ago, I met with Kevin King, Column’s Head of Brand and Communications, to discuss how we wanted to rework our process for making changes to our marketing site, as it was clear that our existing approach would not scale.

We aligned on four core requirements:

  1. The marketing team must be able to easily make changes fully decoupled from our sprint cycles, in a completely self-service capacity.
  2. The development team must be able to maintain its existing deployment pipeline for our core application without change.
  3. Our marketing site needs to live on the same subdomain as our core application, and users shouldn’t have to open up a new tab to access our application. This is relatively uncommon but is a part of the onboarding experience that we are opinionated about.
  4. Both our marketing site and core bundle must be performant and any choice cannot degrade performance.

After a good chunk of research, and some meaningful trial and error, we released a Javascript-based reverse-proxy on top of Cloudflare Workers. This blog walks through the exploration process we went through to land on that decision, as well as details some of the technical choices we made along the way.

Step 1: Can we host two static bundles from the same domain?

The first question that we had to answer was can we effectively host two static bundles under the same subdomain. If we had simple reverse-proxy setup like Nginx, this question would be trivial! However, Column is entirely serverless and our static files are served by Firebase hosting. As a result, we’d never needed to set up a reverse proxy, and would need to be more creative in our approach. The diagram below shows the setup that we wanted:

After exploring different options for serverless reverse proxies, we decided to build a proof of concept using Cloudflare Workers. We already make heavy use of workers in other parts of our network stack so if they could rise to the challenge here, they would be the natural choice.

Using Workers.dev we were able to get a POC up in just over an hour that effectively proxied the request to the correct bundle based on the pathname of the request. Our code looked largely like the following:

One item that continues to blow our minds is that SSL just works out of the box. Because Cloudflare workers sit behind a single Cloudflare Edge SSL Certificate, we were able to terminate SSL at the worker level and then proxy requests to our two different bundles, each with different SSL certs, all while maintaining a single cert presented to the client.

At this point we were confident that we’d be able to effectively serve two bundles from the same domain.

Step 2: Determine a CMS for the marketing team

Once we had confirmation that we could proxy requests through these two different bundles, the next question was what CMS the marketing team should use to build out the site. As our team and the marketing agency Maven Creative we were working with had the most experience with WordPress, it was a relatively easy decision. We decided on WordPress, hosted in Wp-Engine. In other words, Kevin King told us he was using WordPress.

The choice of WordPress immediately raised questions for how we would manage the site setup. Most pressingly, we realized that setting WP_HOME to our actual url (in this case testing.column.us) immediately led to an infinite loop as our worker was sending requests to a a DNS record that pointed back to the worker. However if we maintained our WP_HOME and WP_SITEURL as a separate URL, all of the links, images and stylesheets would point to URLs that looked like website.wpengine.com.

When Kevin correctly pointed out that the proposal to hardcode links to all stylesheets and external static went against the “easy” part of the requirement that “the marketing must be able to easily make changes fully decoupled from our sprint cycles,” we dove deeper into Cloudflare Workers functionality to see how we could dynamically re-write links pointing to website.wpengine.com to our marketing site’s hostname.

We implemented this by making use of the HTMLRewriter API which seems to have been purpose-built for this use case. The Rewriter API allowed us to write code like the following and magically fix all of our links without measurably impacting latency:

Having confirmed that we could proxy static WordPress content, the next step was to productionize our code and get ready for launch.

Step 3: Implementing Kevin, our reverse-proxy

In honor of Kevin King, who bought in to the idea that we could pull off this reverse proxy with no previous proof that it would work, we decided to name the repo where we stored this worker after him. We created a wrangler project, implemented our worker, and used Github Actions + the Wrangler CLI to automatically deploy new changes pushed to our main branch to production.

As we were preparing for launch week, we realized that we had overlooked a critical issue around persistent sessions. Previously, as our marketing site lived inside the same static bundle as our core application, it was easy for us to implement routing logic based on a user’s authentication status. If a user was already logged in when they visited the marketing site, we would bring them directly to their home page. However, as this was no longer the same bundle, those redirects stopped working. If we wanted to maintain this experience, we would have to figure out a way to manage our authentication state in our WordPress site.

Luckily our HTMLRewriter came to the rescue again. We updated our worker to dynamically inject a script into the page that checks the Firebase authentication status into the body of our html. And because our marketing site was still on the same domain as our application bundle, we could access browser storage to determine a user’s authentication status even though logins were taking place in an entirely different bundle.

And with that change implemented, we were ready to launch! There were a few additional tweaks to our HTMLRewriter, but nothing that couldn’t be handled by a JIRA board and a sprint cycle. Our kevin repository was officially in a steady state.

What comes next?

So far this architecture has served us well! While it may have involved a few late nights, we met all of the requirements laid out at the start of this project. You can check out the results for yourself here. There are still a few changes on the horizon, largely around SEO. But we’re confident that this architecture will scale for this next chapter of our marketing site.

If these types of challenges interest you, or you have strong thoughts on why the decisions we made were silly, definitely check out our careers page! We would love to learn from you.