Save $100/mo with custom domains on Vercel

I love the idea of review apps or pull request.  Where your code in a pull request is server live.

I had recently changed the backend of our services to only issue cookies on our personal domain, let's call it myapp.com.  Since we were using Heroku and later Vercel that means .herokuapp.com and .vercel.app were no longer usable suffixes for our review apps.

Vercel offers a custom domain option for $100/month, which seemed rather expensive.  So I decided to see if I could do this on my own.  Vercel also lets you "alias" any domain to a review app.  I'm happy to pay for things, so I can get on with my life, but this seemed too trivial.

tl;dr I used a Github Action to coordinate this.

Our situation

We had a small team of 5 engineers (for now).  We wanted a system that:

  1. Automatically create deployments from pull-requests.
  2. Use a custom domain for said deployments to match our cookies.
  3. Was completely automatic.

So if my coworker made a pull request, they would get a custom instance of their site hosted at something.myapp.com and it could talk to the backend server backend.myapp.com.

DNS

The first step was DNS.  For each engineer I created a DNS record github_username.myapp.dev.  I pointed these all to the Vercel cname, and then I created these as domains for my app in Vercel.

Ultimately these would be the deployment targets that we would hit.

Github Actions

Github has various events which it can listen to.  The event we were listening to was called deployment_status.  Specifically we were waiting for the Vercel deployment to complete.  At which point we would do our aliasing work.

Github let's you run jobs conditionally, this was our above condition as code:

    if: |
      github.event.deployment_status.state == 'success' &&
      endsWith( github.event.deployment_status.target_url, '.vercel.app')

We had 4 general steps we wanted to do:

  1. Find the current user.
  2. Run the alias command (via the Vercel CLI)
  3. Create a new "deployment" in Github for our custom domain.
  4. Mark said deployment as successful.

Finding the current user

I found Github Actions to be pretty powerful, in the way that bash is powerful... you can do whatever you want and there's no prescribed "right" way.  So here's how I found the creator of a pull-request from a deployment_status event:

     - uses: octokit/request-action@v2.x
       name: Find Author
       id: get_author
       with:
         route: GET /repos/${{ github.repository }}/commits/${{ github.sha }}
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This uses the Github API (which is well documented) to give you data about the commit that was deployed (github.sha).  From that we are able to get the author's Github username by doing something like:

${{ fromJson(steps.get_author.outputs.data).author.login }}

Running the alias command

      - uses: emregency/vercel-preview-alias@v1.5
        name: Make Alias
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
          vercel-preview-url: ${{ github.event.deployment_status.target_url }} #Required
          vercel-target-url: ${{ fromJson(steps.get_author.outputs.data).author.login }}.myapp.com #Required

This action calls the alias command that you get from the vercel CLI.  It takes an existing preview URL which we get from the deployment_status event that triggered this action and you give it your target URL.  We are going with github_username.myapp.com as mentioned above.  It's very fast as almost everything in Vercel is.

Making a deployment and marking it as success

To really clean things up and potentially trigger other actions (e.g. Calibre) we create a new deployment and deployment status.  This let's people get the new .myapp.com link for their pull request:

	  - uses: octokit/request-action@v2.x
        name: Create Deployment
        id: deployment
        with:
          route: POST /repos/${{ github.repository }}/deployments
          ref: ${{ github.sha }}
          environment: Preview
          auto_merge: false
          description: Deploy to ${{ fromJson(steps.get_author.outputs.data).author.login }}.tome.fans
          required_contexts: '[]'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: octokit/request-action@v2.x
        name: Set Deployment Status
        id: deployment_status
        with:
          route: POST /repos/${{ github.repository }}/deployments/${{ fromJSON(steps.deployment.outputs.data).id }}/statuses
          state: success
          target_url: https://${{ fromJson(steps.get_author.outputs.data).author.login }}.tome.fans
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This makes two calls to the Github API, one to create a deployment, and a second to set it as successful.  

This wraps up the code into a nicely executing workflow.

Sustainability

While it's kind of fun to spend lots of hours trying to save a few dollars, I felt like there were little parts of this work-flow that could go wrong later and I didn't want to maintain it.  So I ultimately ended up paying for this service and deleting a file from our repository.  If you looked closely, our work around provisioned one domain per engineer, which often wasn't enough for our team members who had many frontend pull requests.

While I really like Vercel, I'm not really a fan of the pricey upgrades.  It's like buying a car and finding out that you need to pay an extra monthly fee to use a feature.  If it really bothers me, I might restructure our app to not used.