The Bottleneck: Slow PR reviews
📷 Picture this: your team just finished implementing a crucial feature. The code looks good, tests are passing, but there's one problem, no one can actually see the feature working without pulling the branch locally or watching static screenshots. Sound familiar?
That was us moments ago. Our review process looked something like this:
- Developer opens a PR
- The team reads the code (🤔 "This looks right...")
- They either approve based on code alone or ask for screenshots
- If screenshots aren't enough, well... We had to make it work somehow.
- Repeat until everyone's happy
The result? Review cycles that took days instead of hours, and not-so-great velocity.
Our first attempt: Front-on-Demand and why it didn't scale
Each team then had one staging environment of the same exact app. When they needed a review and when their staging environment was in use, they were able to manually push the app to it. We called it "Front-on-Demand" (creative, right?).
This worked... sort of. The problems quickly became apparent:
- Sequential bottleneck: Only one branch could be deployed at a time per team
- Manual overhead: Devs had to remember how to deploy from their laptops
- Resource waste: Staging environments sitting idle or forgotten
We needed something better.
The Breakthrough: PR Previews on Autopilot
The turning point came when we decided to treat PR previews as a first-class citizen in our CI/CD pipeline. Our goal was simple, every PR should automatically get its own preview URL quickly and be cleaned up automatically, of course.
Here's the workflow:
Nx magic: our publish-preview executor
Instead of writing custom deployment scripts for each app, we created a single, configurable Nx executor that works across our entire mono-repo & easy to enroll for new apps. It handles the entire preview deployment pipeline based on simple steps:
- Determine current branch (default or not)
- Get the current PR
- Get the built app and upload it to S3 using the AWS CLI
- Comment the PR to let the user know we've deployed/updated the preview environment of the PR.
What Nx brings to the table
Before we dive into the AWS infrastructure, let's pause and appreciate what Nx brought to the table:
- 🏗️ Monorepo Architecture: Nx understands dependencies between apps and libraries, making it perfect for complex monorepos where changes ripple across multiple applications.
- 🎯 Affected Project Detection: Nx automatically identifies which apps are impacted by your changes. No more manual configuration or complicated guessing, Nx figures it out for you.
-
🔧 Executor System: Our publish-preview can be configured per project and is automatically inferred by a local plugin. To enroll a new app, you simply add a metadata flag like
"previewDeploy": "true"
in theproject.json
file and you’re basically done !
To power all of this, we run a single command in CI: yarn nx affected -t publish-preview
Infra magic with AWS: S3 + CloudFront + Lambda@Edge
The infrastructure magic happens through a combination of S3, CloudFront, and Lambda at edge. Of course there is also a WAF and some other things that we won’t be talking about for simplicity’s sake.
Our Nx executor uploads each PR's build to a specific S3 path:
// For each PR, each affected app gets its own S3 path: {pr-number}-{app-name}
const s3Path = `s3://${options.bucketName}/${pr.number}-${context.projectName}`
// Upload with no-cache headers for immediate updates
await exec(`aws s3 sync ${distPath} ${s3Path} --cache-control "max-age=0"`)
Then, thanks to both our lambdas at edge we secure the CloudFront endpoint (which is public by default) & rewrite the path to be able to use clean URLs like https://{pr-number}-{app-name}.preview.my-domain.com
. The path rewrite is looking something like this:
const host = request.headers.host?.[0]?.value
const subdomain = host.split('.')[0]
if (subdomain) {
request.uri = `/${subdomain}${request.uri}`
return callback(null, request)
}
return callback('Missing subdomain')
This means that we have:
- PR #123 for app foo →
https://123-foo.preview.my-domain.com
- PR #456 for app bar →
https://456-bar.preview.my-domain.com
Our Lambda@Edge function routes these custom domains to the correct S3 paths.
This gives us clean, predictable URLs for every PR without any manual intervention. Using this setup we are able to support nearly an unlimited amount of apps using a couple of lambdas, a single CloudFront, a single S3 bucket and a single Nx cli command.
We learned to really value short feedback loops and giving users clear solid feedback about what’s going on. We are pushing a message in the deployed PR each time we deploy or update the affected apps:
Outcomes: Faster Reviews, Happier Developers 📈
The impact was immediate:
- 🕐 Review time: while we weren’t able to measure it precisely, it dropped significantly
- 💪 Confidence: Reviewers could actually see and test changes in a couple of minutes tops !
But the best metric? Developer satisfaction. No more "Can you deploy this so I can see it? Is the environment free?" messages in Slack.
Building an automated PR preview system transformed how our team reviews code for frontend apps. What started as a manual, error-prone process became a smooth, automated experience that developers actually love using.
The key was treating previews and developer experience not as an afterthought, but as a core part of our development workflow. With the right tooling (Nx executors, Nx cloud cache), infrastructure (AWS Lambda + CloudFront), and automation (Nx agents), we created a system that scales with our team and keeps everyone moving fast, taking less than 2 minutes to deploy an up-to-date app. And there is nearly 0 maintenance time.
What's Next: Towards a Developer Platform
Over at Payfit we're building our developer platform based on Nx and custom plugins, allowing us to manage the whole CI pipeline easily and publishing artefact simply using nx release
, which will be the subject of another blog post pretty soon!
👂 I'd love to hear your thoughts! Drop a comment below and let me know if you've implemented similar systems. What tools and strategies have you found effective?
Top comments (0)