QA’ing pull requests using Heroku Review Apps

Not too long ago, Cushion’s build and deployment process was migrated over to Heroku CI, which runs through Cushion’s tests and builds the front-end, but also provides a staging pipeline with one-click promoting to production. It’s been an absolute dream compared to the previous service we used, which felt like it was held together with duct tape. Ever since using Heroku CI, one of its most useful features, Review Apps, has been staring at me for years, unused, until recently.

Review Apps let you automatically deploy a disposable staging environment for new pull requests, instead of merging your code into a single staging branch in order to QA it in a production-like environment. It sounds (and is) magical, but didn’t seem possible given Cushion’s subdomain routing—the main app is served from and the signup routes are served from Because of this, Cushion can’t route properly from the * URL that’s assigned to it, so it also won’t work for the URLs that are generated for each pull request’s Review App.

For years, I assumed it was either impossible with Cushion’s setup or simply required more time than I could afford to tackle the problem, considering the mountain of other to-dos on my list, but then I came across this article about setting up Heroku Review Apps with custom domains. The article details an approach using the DNSimple API and Heroku Platform API to automatically create a new CNAME record with the DNS target from the Review App when it spins up. This was definitely the direction I needed to go in, but tweaked to handle multiple subdomains and setting up wildcard SSL for these subdomains.

For multiple subdomains, I simply looped through a list of Cushion’s subdomains instead of only targeting a single one. The variable subdomains were tricky, though, because I essentially needed to modify how I specify subdomains across the entire app. Up until now, I simply hard-coded it to check for my. or get., which is obviously the most flexible, future-proof approach... With Review Apps, however, a PR-specific subdomain is generated for you, so you can only predict it to a certain degree—it still has an auto-incrementing variable pointing to the PR.

When I originally set up the staging environment, I used an environment variable to let me host Cushion on different domains. For production, the variable is and for staging, it could be something like (it’s not Since the routing and linking uses the environment variable, everything automatically points to the host I specify. This works just as well for the subdomains.

For production and staging, I created new environment variables for the app subdomain and signup subdomain that are simply my and get respectively. Then, for the Review Apps, I set these variables to the generated subdomains, but prefixed with my- and get-, so I’m left with something like All I had to do then was use the Heroku Platform API to create the SNI endpoint for SSL, using the wildcard SSL cert I got from DNSimple.

I ended up refactoring the rake tasks from the article quite a bit. I made the tasks into their own testable class where I can call ReviewApp.setup and ReviewApp.cleanup. I also removed the Heroku request to get the CNAME because the domain.create request already returns the CNAME. Lastly, I added a TTL of 60 to the DNSimple records, so I don’t need to wait if a Review App’s record needs to be updated.

I’ve become somewhat addicted to improving the dev experience for Cushion. I didn’t feel like I could afford to do it while sprinting month-to-month, but now it feels possible without any urgency or pressure. Since I know that Cushion will continue to be my side project for years to come, investing in these improvements now really helps reduce any friction or burden from updating the app. One of the worst things that can happen to an app is to reach a point where it’s so brittle that you’re afraid to touch it. Instead, you should strive for a constant state where your app is so solid and reassuring that you have the utmost confidence to make changes.