<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tom Masson</title>
    <description>The latest articles on DEV Community by Tom Masson (@tom-masson).</description>
    <link>https://dev.to/tom-masson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3538999%2Fb63ee911-4a1c-4b91-892e-c51a7a89b7d9.jpg</url>
      <title>DEV Community: Tom Masson</title>
      <link>https://dev.to/tom-masson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tom-masson"/>
    <language>en</language>
    <item>
      <title>Prepping the Ingredients: Scaling CI with a Unified Monorepo Engine</title>
      <dc:creator>Tom Masson</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:27:02 +0000</pubDate>
      <link>https://dev.to/tom-masson/prepping-the-ingredients-scaling-ci-with-a-unified-monorepo-engine-m0k</link>
      <guid>https://dev.to/tom-masson/prepping-the-ingredients-scaling-ci-with-a-unified-monorepo-engine-m0k</guid>
      <description>&lt;p&gt;In the previous post, I talked about how we stopped building and started &lt;em&gt;listening&lt;/em&gt; to our developers. We uncovered a fragmented CI/CD landscape riddled with challenges: a glaring lack of standardization and uniformization across teams, stability issues and performance bottlenecks that hindered developer velocity. The loudest complaint? Migration fatigue. Every time the platform team "improved" something, it meant hours of manual work for every product team.&lt;/p&gt;

&lt;p&gt;To fix this, we didn't just need better pipelines; we needed a way to automate the "tax" of being on the platform and provide a robust, unified CI/CD experience. Just as importantly, we needed to build those new foundations in public, alongside the teams who would eventually rely on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Brain of our Monorepo: Implementing Nx and our custom Nx plugin
&lt;/h2&gt;

&lt;p&gt;We took the opportunity of our ongoing shift toward &lt;strong&gt;Domain Driven Design (DDD)&lt;/strong&gt;, organizing our code into monorepos per business domain, to make sure to leverage Nx to act as the central orchestrator of our backend monorepos. This was not a gamble considering &lt;a href="https://beaussan.io/talks/2026/react-paris" rel="noopener noreferrer"&gt;the &lt;strong&gt;frontend track record&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our magic sauce, the custom @payfit/nx-core Nx plugin
&lt;/h3&gt;

&lt;p&gt;This plugin is the engine that drives our CI platform. We didn't just write one off scripts, we built a triad of automation leveraging &lt;strong&gt;native Nx primitives&lt;/strong&gt;. This is a crucial distinction, because we are using the same tools and patterns that our developers already know, the platform isn't a &lt;strong&gt;"black box"&lt;/strong&gt;, it's a transparent, customizable extension of their existing workflow.&lt;/p&gt;

&lt;p&gt;That transparency mattered as much as the technical implementation itself. We were dogfooding the same setup in our own repositories, cleaning up our own workflows first, and sharing progress publicly as we went. Before inviting teams onto the new paved road, we wanted to make sure we were already walking it ourselves.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic configuration&lt;/strong&gt; &lt;strong&gt;with &lt;a href="https://nx.dev/concepts/inferred-tasks" rel="noopener noreferrer"&gt;Nx Inferred Tasks&lt;/a&gt;&lt;/strong&gt;: By leveraging the &lt;a href="https://nx.dev/features/manage-releases" rel="noopener noreferrer"&gt;&lt;strong&gt;Nx Release SDK&lt;/strong&gt;&lt;/a&gt;, we inferred a standardized &lt;code&gt;nx-release-publish&lt;/code&gt; target for every publishable project. This allowed us to orchestrate the entire versioning, changelog generation, and publishing lifecycle in a single, atomic operation. Developers don't have to manually configure these targets, they just "appear" when the project meets the criteria.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nearly zero maintenance tax with &lt;a href="https://nx.dev/docs/features/automate-updating-dependencies" rel="noopener noreferrer"&gt;Nx Migrations&lt;/a&gt;&lt;/strong&gt;: When we need to roll out a breaking change, we write a Nx migration. These are automated code mods that could update literally anything in the monorepos and apply fixes at scale, turning &lt;strong&gt;daunting manual migrations into simple PR reviews&lt;/strong&gt;, automatically opened and applied by a dependency bumping tool.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated installation with &lt;a href="https://nx.dev/docs/features/generate-code" rel="noopener noreferrer"&gt;Nx Init Generators&lt;/a&gt;&lt;/strong&gt;: Our &lt;code&gt;init&lt;/code&gt; generator handles the "day zero" setup, installing necessary dependencies and configuring plugin settings in one command.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated drift detection with &lt;a href="https://nx.dev/docs/concepts/sync-generators" rel="noopener noreferrer"&gt;Nx Sync Generators&lt;/a&gt;&lt;/strong&gt;: Our &lt;code&gt;sync&lt;/code&gt; generators ensure that &lt;strong&gt;config drift&lt;/strong&gt; is eliminated by keeping local configurations in sync with reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated conformance across our org using &lt;a href="https://nx.dev/docs/reference/conformance/overview" rel="noopener noreferrer"&gt;Nx Conformance&lt;/a&gt;&lt;/strong&gt;: We wrote several global conformance rules that all our repositories must follow. Nx Conformance ensures they comply automatically and fails fast in PRs whenever someone breaks a rule, with a clear error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient &amp;amp; performant by default&lt;/strong&gt;: We only run Nx CLI commands (like &lt;code&gt;nx affected&lt;/code&gt;), the system only builds, tests, and releases what actually changed. Nx targets are run in parallel by default.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI vendor agnostic&lt;/strong&gt;: Our logic lives in Nx executors and we only run CLI commands, we are decoupled from the underlying CI vendor. Whether we are running on CircleCI, GitHub Actions, or locally on a developer's machine, the behavior is identical. The workflow is the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Under the Hood: The Polyglot Publisher
&lt;/h2&gt;

&lt;p&gt;For my tech folks, here’s a brief deep dive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you’re interested in the implementation details, I may put together a follow-up article that explores it in depth, let me know if that’s something you’d like to see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We replaced a dozen of "snowflake" custom release scripts and CircleCI Orbs with a single, unified Nx executor: &lt;code&gt;@payfit/nx-core:publisher&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F465bq6eiomakyo2scsv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F465bq6eiomakyo2scsv2.png" alt="Mermaid for CI pipeline" width="600" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker images were a major source of CI bloat. We reworked the process using our &lt;code&gt;docker-build&lt;/code&gt; executor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimized lightweight Dockerfiles&lt;/strong&gt;: Instead of the naive &lt;code&gt;COPY . .&lt;/code&gt; of the whole workspace, we copy only the specific files needed for that project's dependency graph, ensuring smaller layers, smaller image size, faster builds, faster Kubernetes pod spin up. Everyone win with lighter images.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Build Once" Rule&lt;/strong&gt;: The Dockerfile reuses the &lt;code&gt;dist&lt;/code&gt; artifacts already generated in previous CI steps, ensuring that what was tested is exactly what is packaged. We also separated the Docker build from the release step. CI builds the image once (tagged with the commit SHA), and the &lt;code&gt;publisher&lt;/code&gt; simply &lt;strong&gt;promotes&lt;/strong&gt; that artifact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Lambda: Smoke Testing as a First Class Citizen
&lt;/h4&gt;

&lt;p&gt;For each Lambda publishing job, we are verifying if it works in a real environment before the pipeline finishes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Pre-flight" Smoke Test&lt;/strong&gt;: Before the &lt;code&gt;publisher&lt;/code&gt; marks a release as successful, it triggers a run on the project's dedicated &lt;strong&gt;Spacelift&lt;/strong&gt; infrastructure stacks. It triggers a deployment and runs a health check to ensure the function is properly "good to go" in a real environment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail Fast&lt;/strong&gt;: If the smoke test fails, the CI job fails immediately. We catch "it works on my machine" bugs before they ever reach our promotion tool, &lt;strong&gt;Kargo&lt;/strong&gt; (which we'll explore in Part 3).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Calendar Versioning for Readability
&lt;/h4&gt;

&lt;p&gt;With over 12,000 deployments a month over a hundred applications, traditional semantic versioning can make it difficult to quickly identify when a change was released. We adopted &lt;a href="https://calver.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Calendar Versioning (CalVer)&lt;/strong&gt;&lt;/a&gt;, using the format &lt;code&gt;YYYYMM.DD.patch&lt;/code&gt;, to provide immediate temporal context for every artifact. This makes our release history significantly more readable and helps teams identify version age at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Impact: Reduced Friction and Enhanced Visibility
&lt;/h2&gt;

&lt;p&gt;Today, CI at PayFit is no longer a collection of "snowflakes", it’s a standardized, stable engine leveraging Nx, the central "brain" that has become our shared language across every repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One Pipeline, One Config&lt;/strong&gt;: Whether you're working on a backend app, a Lambda, or a frontend app, the pipeline is the same.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Paved Road, Not a Walled Garden&lt;/strong&gt;: Everything we've built is through native Nx support. This means teams aren't locked into a rigid platform, they can easily override, extend, or customize their configurations whenever they need to, using the same mental model they use for their local development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;strong&gt;five months&lt;/strong&gt;, we migrated &lt;strong&gt;90 applications&lt;/strong&gt; to this new standard. But those migrations didn't succeed because we forced them through. We started with a small number of teams with whom we already had strong relationships, gave them our full attention, and treated every issue they hit as a platform bug we had to fix quickly. Those early adopters helped us harden the system in real conditions.&lt;/p&gt;

&lt;p&gt;That is what gave us the right to scale. By leveraging &lt;strong&gt;Nx's remote caching&lt;/strong&gt; and optimizing our pipelines, &lt;strong&gt;we nearly halved the CI time for every repository we migrated.&lt;/strong&gt; Our &lt;strong&gt;deployment frequency increased by 20.8x&lt;/strong&gt;, jumping to over &lt;strong&gt;12,000 deployments a month&lt;/strong&gt;, we regained our leverage as a Platform. We now have an entrypoint on each monorepo, allowing us to provide an easy, automated migration path moving forward.&lt;/p&gt;

&lt;p&gt;At the beginning, we were the ones reaching out and asking teams whether they wanted to move. A few months later, the situation had flipped, teams were asking to migrate faster than we could support them. That backlog of demand was one of the strongest signals that the platform had stopped being perceived as a constraint and had started to be seen as an accelerator.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lead time visibility
&lt;/h4&gt;

&lt;p&gt;The equally impactful outcome was the radical increase in &lt;strong&gt;visibility&lt;/strong&gt; across the entire delivery lifecycle. Previously, we simply didn't have the data to measure &lt;strong&gt;DORA metrics&lt;/strong&gt; accurately. By standardizing on a single pipeline, we've finally gained the ability to track the &lt;strong&gt;entire journey&lt;/strong&gt;. Our median lead time for change currently sits at &lt;strong&gt;16.19 hours&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Is that number high? Maybe. But for the first time, it's a &lt;strong&gt;real&lt;/strong&gt; number. It's not a vanity metric, it's a reflection of our actual engineering cycle. Now that we can finally see the full picture, we have the baseline we need to identify real bottlenecks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Unified Scaffolding: The Next Step
&lt;/h4&gt;

&lt;p&gt;We still have some manual steps in the creation of new apps. Our next step is to leverage &lt;strong&gt;Nx generators&lt;/strong&gt; to provide full, opinionated scaffolding for backends, frontends, and Lambdas, with the goal to provide a single, unified CLI where a developer can run a command and have a production ready, fully integrated app in minutes. Because we've already standardized the underlying engine, these generators will unlock a "Ready to Ship" state by default, with no configuration required.&lt;/p&gt;

&lt;p&gt;But CI is only half the battle. Getting those artifacts into production safely across 100+ apps requires a different kind of magic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the final part, I’ll explain how we used Kargo to close the CD loop and how we built an automated safety net to ensure every release is production ready.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>productivity</category>
      <category>devex</category>
    </item>
    <item>
      <title>The "Fruit Basket" problem: Rebuilding PayFit's platform trust &amp; alignment</title>
      <dc:creator>Tom Masson</dc:creator>
      <pubDate>Fri, 10 Apr 2026 08:44:03 +0000</pubDate>
      <link>https://dev.to/tom-masson/the-fruit-basket-problem-rebuilding-payfits-platform-trust-alignment-3ena</link>
      <guid>https://dev.to/tom-masson/the-fruit-basket-problem-rebuilding-payfits-platform-trust-alignment-3ena</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;If I have to touch infrastructure, I double my estimate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In early 2025, that was the prevailing sentiment among developers at PayFit. It wasn't that we didn't have tools, we had plenty. We also had a platform team. Actually, we had five of them.&lt;/p&gt;

&lt;p&gt;And that was exactly the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Organizational Pendulum
&lt;/h2&gt;

&lt;p&gt;Between &lt;strong&gt;2019 and 2022&lt;/strong&gt;, PayFit swung between two extremes:&lt;/p&gt;

&lt;p&gt;During our hyper growth phases, we’d have a centralized platform team building abstractions in a vacuum. By the time COVID hit, the pendulum swung the other way, we’d embed SREs directly into product teams to "bring DevOps culture", only to find they were being pulled into daily firefighting instead of building leverage.&lt;/p&gt;

&lt;p&gt;We oscillated between monorepos and multi repos. Every swing left behind "ghost" migrations, half-finished initiatives from &lt;strong&gt;2021 or 2022&lt;/strong&gt; that developers were still forced to maintain in &lt;strong&gt;2024&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Scorecard" Era
&lt;/h2&gt;

&lt;p&gt;Trust reached an all time low during what I call the "Scorecard Era" around &lt;strong&gt;2023&lt;/strong&gt;. We had SREs embedded in teams, but their objectives were often conflicting. While the product team was trying to ship a critical feature, the SRE was focused on checking boxes on a platform scorecard, metrics that mattered to the platform team but felt like a pain to the developers with no immediate business impact.&lt;/p&gt;

&lt;p&gt;We had four or five different platform related teams, and if you asked them how to deploy a new service, you might get four or five different answers. &lt;a href="https://architectelevator.com/book/platformstrategy/" rel="noopener noreferrer"&gt;Gregor Hohpe&lt;/a&gt; (who explores the "Platform as Product" mindset in his book Platform Strategy) has a great analogy for this: &lt;strong&gt;fruit basket vs. fruit salad&lt;/strong&gt;. A "fruit basket" is a collection of whole fruits, powerful tools, but you have to peel, chop, and mix them yourself. A "fruit salad" is the integrated experience where the platform team has done the heavy lifting for you.&lt;/p&gt;

&lt;p&gt;We weren't providing a fruit salad. We were throwing a heavy fruit basket at developers, and it was full of pips.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Voice, One Team
&lt;/h2&gt;

&lt;p&gt;The biggest change wasn't technical. It was organizational. By &lt;strong&gt;late 2024&lt;/strong&gt;, we had finally stopped the fragmentation. We consolidated those 4-5 different teams into a single &lt;strong&gt;Internal Developer Platform (IDP)&lt;/strong&gt; team.&lt;/p&gt;

&lt;p&gt;This "one voice" speaking with a single roadmap and source of truth was the prerequisite for everything that followed. It gave us the opportunity to stop the "pendulum swings" and adopt a true "Platform as Product" mindset.&lt;/p&gt;

&lt;p&gt;We also had a rare alignment in leadership: a shared conviction that a strong platform is a competitive advantage, and a collective willingness to prioritize long term stability over immediate feature velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Frontend Precedent
&lt;/h2&gt;

&lt;p&gt;Trust isn't built with slides, it’s built with proof. Our frontend teams had already moved to an &lt;strong&gt;Nx&lt;/strong&gt; monorepo and were seeing massive benefits in caching and developer ergonomics (a journey &lt;a href="https://nx.dev/blog/payfit-success-story" rel="noopener noreferrer"&gt;featured on the Nx blog&lt;/a&gt; and &lt;a href="https://twitter.com/beaussan" rel="noopener noreferrer"&gt;Nicolas Beaussart&lt;/a&gt; just gave &lt;a href="https://beaussan.io/talks/2026/react-paris" rel="noopener noreferrer"&gt;a great talk over at the React 2026 conf in Paris&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;They definitely opened the door and led the way for the rest of us. By the time we proposed the same model for backend and infrastructure, we weren't asking for a "leap of faith", we were simply following a path they had already cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Change Policy: Paved Road over Mandate
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;"one voice"&lt;/strong&gt; consolidation wasn't just about efficiency, it was about strategy. It allowed us to shift from a mandate driven culture to a "Paved Road" model.&lt;/p&gt;

&lt;p&gt;We stopped trying to force teams to migrate. Instead, we focused on making the new Internal Developer Platform so much faster, more stable, and more intuitive that it became the path of least resistance. When the &lt;a href="https://netflixtechblog.com/full-cycle-developers-at-netflix-a08c31f83249" rel="noopener noreferrer"&gt;&lt;strong&gt;Paved Road&lt;/strong&gt;&lt;/a&gt; is 10x better than the legacy "Dirt Road", you don't need a mandate, teams will naturally choose it. This reduced organizational friction and let us focus on building a product that developers actually wanted to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turning Point: The Interview Tour
&lt;/h2&gt;

&lt;p&gt;With a unified team finally in place, we realized we couldn't just "tool" our way out of the remaining trust deficit with the teams. We stopped building and started listening.&lt;/p&gt;

&lt;p&gt;Throughout &lt;strong&gt;H1 2025&lt;/strong&gt;, we conducted an "Interview Tour". We identified key players across every domain &amp;amp; team and conducted 45 minutes deep dives. We didn't ask "what tools do you want?" We used a set of prepared, open ended questions to drive the discussion and collect essential qualitative feedback on "where it hurts."&lt;/p&gt;

&lt;p&gt;The feedback was raw:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"If I have to touch infrastructure, I double my estimate."
&lt;/li&gt;
&lt;li&gt;"Migrations feel like a trap because they never end."
&lt;/li&gt;
&lt;li&gt;"I don't know who to talk to when my pipeline breaks."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product first mindset:&lt;/strong&gt; We treated our developers as customers, not captive users. We rebuilt the platform foundations in public, starting with CI/CD, and made the work visible as it happened instead of revealing it only once it was finished.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working with teams, not for teams:&lt;/strong&gt; We kept talking to users continuously, shared wins publicly, gave kudos for every contribution, and backed the story with concrete data. The goal was not to build a platform &lt;em&gt;for&lt;/em&gt; developers in isolation, but to build it &lt;em&gt;with&lt;/em&gt; them.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A better developer experience:&lt;/strong&gt; We focused relentlessly on the basics developers actually feel every day, faster workflows, more reliable pipelines, and less platform friction. That also meant cleaning up our own setup first and dogfooding the same repository patterns, tooling, and workflows we wanted other teams to adopt. In other words, we cleaned our house before inviting guests.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting with the right early adopters:&lt;/strong&gt; We deliberately began with a handful of teams we already had strong relationships with. We gave them our full attention, treated their issues with the new system as our top priority, and fixed problems fast. That was the least we could do: they were trusting us with a brand new platform, and their feedback directly helped us improve it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The turning point:&lt;/strong&gt; At first, we were the ones asking teams whether they wanted to migrate. Then, gradually, the dynamic flipped. More and more teams wanted in, to the point where demand started to outpace our capacity to support migrations. That was a great problem to have, and the clearest signal that trust was coming back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By shifting from "Platform doing things TO us" to "Platform working WITH us", the narrative started to change. We didn't create momentum with mandates. We created it by listening, improving the house, and proving that the paved road genuinely delivered better DX, faster workflows, and more reliable outcomes.&lt;/p&gt;

&lt;p&gt;But to deliver on the promise of a "simpler &amp;amp; faster workflow", we had to tackle one of the monsters under the bed: CI/CD complexity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In Part 2, I’ll dive into how we rebuilt our CI pipeline with Nx to drive standardization, performance, and stability across the entire organization.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>idp</category>
      <category>devex</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How 3 friends created and published a video game in less than a year without writing a single line of code ?</title>
      <dc:creator>Tom Masson</dc:creator>
      <pubDate>Fri, 03 Apr 2026 13:24:13 +0000</pubDate>
      <link>https://dev.to/tom-masson/how-3-friends-created-and-published-a-video-game-in-less-than-a-year-without-writing-a-single-line-4in2</link>
      <guid>https://dev.to/tom-masson/how-3-friends-created-and-published-a-video-game-in-less-than-a-year-without-writing-a-single-line-4in2</guid>
      <description>&lt;p&gt;About a year ago, two friends and I casually discussed the idea of creating a video game. We did not know what we wanted to do nor how to do it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Where do we start ?
&lt;/h1&gt;

&lt;p&gt;So to begin with, we started to review all the video games genres we liked, to see if we had any in common. From that, we quickly studied the current business situation of the party games as that was what we opted for. It seemed to us that it was kind of an opportunity as there were &lt;strong&gt;not many party games out there&lt;/strong&gt; although being a niche genre.&lt;/p&gt;

&lt;p&gt;We created a company to be able to &lt;strong&gt;share hypothetical revenues fairly&lt;/strong&gt; and had to find a bank to host our company account.&lt;/p&gt;

&lt;p&gt;After discussing all of this, we were ready to start and deep dive in the actual project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Actually starting
&lt;/h1&gt;

&lt;p&gt;So we knew the genre of game we wanted to make but how do we actually create a video game ? &lt;/p&gt;

&lt;p&gt;We started to study game engines, how to publish a game and on which platform (&lt;a href="https://partner.steamgames.com/steamdirect" rel="noopener noreferrer"&gt;Steam&lt;/a&gt;, &lt;a href="https://dev.epicgames.com/docs/epic-games-store/publishing-tools/publishing-process/publishing-workflow" rel="noopener noreferrer"&gt;Epic&lt;/a&gt;, &lt;a href="https://itch.io/developers" rel="noopener noreferrer"&gt;itch.io&lt;/a&gt;, …) as both are linked. We did want to publish it on PC and most likely on Steam because basically everyone has been using this platform for years. &lt;/p&gt;

&lt;p&gt;For game engines, &lt;a href="https://godotengine.org/" rel="noopener noreferrer"&gt;Godot&lt;/a&gt; was too young, &lt;a href="https://unity.com/" rel="noopener noreferrer"&gt;Unity&lt;/a&gt; just released a statement to make the developers give them more money, so we were left with &lt;strong&gt;&lt;a href="https://www.unrealengine.com/" rel="noopener noreferrer"&gt;Unreal Engine&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We did a quick review of all the technologies and languages offered to us by Unreal Engine and ended up choosing to use the visual scripting tool called &lt;strong&gt;&lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/blueprints-visual-scripting-in-unreal-engine" rel="noopener noreferrer"&gt;Blueprints&lt;/a&gt;&lt;/strong&gt;, which let us create game logic without needing to code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgxy9hq8vs6rr8jzobzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgxy9hq8vs6rr8jzobzv.png" alt="Blueprint no code example" width="800" height="684"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Blueprint no-code example&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To be fair, using Blueprints is coding visually with an object oriented language but it made our lives easier and we did not have to learn C++ which would have been much more time consuming. And spoiler alert, we still think we made &lt;strong&gt;the right choice&lt;/strong&gt; on this. Yes we saw limitations but the gain of time and productivity is worth it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Building the game
&lt;/h1&gt;

&lt;p&gt;The first steps were &lt;strong&gt;pretty intimidating&lt;/strong&gt;. But after following several tutorials found on the web, we were getting a little bit used to UE 5. Then we started prototyping several ideas of mini games we had.&lt;/p&gt;

&lt;p&gt;The workflow we found the most effective was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone has an idea of a mini game&lt;/li&gt;
&lt;li&gt;We all discuss it to find whether or not it's worth developing&lt;/li&gt;
&lt;li&gt;We develop a quick &amp;amp; dirty prototype, without any 3D asset or map &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flea5chh0v2jxiwuh63v7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flea5chh0v2jxiwuh63v7.png" alt="Prototype of a mini game" width="800" height="323"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Prototype of a mini game&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We test it together to evaluate the potential of the gameplay, ask feedback to friends, re-assess the idea, think about potential functionalities&lt;/li&gt;
&lt;li&gt;Finally if its a go, we break down everything in simple tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We tried around &lt;strong&gt;25 different ideas&lt;/strong&gt; and finally released around &lt;strong&gt;15&lt;/strong&gt;. Not all the ideas were good or easy to develop. We had to find a balance between fun, development time and skills. &lt;/p&gt;

&lt;p&gt;We started by creating simple mini games like giving a bomb that is randomly exploding to other players by colliding with them. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h2p74z5wzrsc5nq09om.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h2p74z5wzrsc5nq09om.gif" alt="Mini game Do not keep the bomb !" width="560" height="243"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Do not keep the bomb !&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And then moved on more advanced ones with physics, enemies using AI and vehicles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzczboxt55jrymgeye7ul.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzczboxt55jrymgeye7ul.jpeg" alt="Mini game Build the highest tower !" width="600" height="337"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Build the highest tower !&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Everything had to be synchronized across &lt;strong&gt;all 8 players&lt;/strong&gt;. It took time and we had to do some technical trade offs but we managed to get a pretty smooth gameplay.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k6024qhhr01gksq5fjt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k6024qhhr01gksq5fjt.gif" alt="Mini game Move the maximum of boxes across the bridge!" width="560" height="243"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Move the maximum of boxes across the bridge!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It wasn’t easy to avoid the common pitfalls that many indie developers face&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is really easy to drown in the possibilities offered by Unreal Engine. It is a really complex game engine to manage and the documentation is not the best, we often found ourselves fiddling around little details that did not matter or investing time in functionalities we did remove at the end.&lt;/li&gt;
&lt;li&gt;Unsuspected technical difficulties like using Git with Unreal Engine which was a pain and cost us a bunch of time and we probably should have used Perforce. The installation of the game engine locally can also be a time consuming event when asking friends to help on the project.&lt;/li&gt;
&lt;li&gt;Try to avoid tunnel vision. As it was a side project and all of us had day jobs, we tried to ensure we all have smooth communication using Discord. But it happened a couple of times when one of us was working real hard on a functionality and stopped giving regular feedback to others. It usually turned out we spent far too much time and effort on something we did not agree on and was not on the roadmap.&lt;/li&gt;
&lt;li&gt;Keep your eye on the target of what you try to achieve: we bought all of our 3D assets from &lt;a href="https://syntystore.com/" rel="noopener noreferrer"&gt;Synty's marketplace&lt;/a&gt; as none of us were able to use any 3D modeling tool. We also bought our animations. We actually gained so much time by not bothering to try and keeping focus on what we tried to achieve: creating a game.&lt;/li&gt;
&lt;li&gt;Respecting the copyright license of each 3D asset, sound, music we have used took time. We had to read in detail what we legally could or couldn't do and keep track of which asset has which license.&lt;/li&gt;
&lt;li&gt;Do not hesitate to leverage the Unreal Engines huge community to ask for help whether on &lt;a href="https://forums.unrealengine.com/" rel="noopener noreferrer"&gt;their forums&lt;/a&gt; or on Discord. People are helping each other and it can save you a day or two of looking around for a solution to solve a weird bug.&lt;/li&gt;
&lt;li&gt;Automate QA, especially when creating a multiplayer game. Writing code, developing functionalities is fun but spending some time investigating automated testing will be worth it in the end as usual in a tech project, even in the video game field. We actually did not use any of the automated testing features provided by Unreal Engine and we end up usually breaking some stuff when releasing updates.&lt;/li&gt;
&lt;li&gt;Leverage technology to gain productivity. We have created ourselves several tools to make our lives easier. For instance we have used a lot of &lt;a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/procedural-content-generation--framework-in-unreal-engine" rel="noopener noreferrer"&gt;Procedural Content Generation&lt;/a&gt; to quickly set up new maps, automatically generating race tracks &amp;amp; decorations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Actually shipping
&lt;/h1&gt;

&lt;p&gt;After months of development, the game was starting to actually look like a real game that people could play. We started to get a bit tired of spending all of our free time on this project and we asked ourselves when we should stop the development and start polishing the game to actually release it. That should be an easy task right ?&lt;/p&gt;

&lt;p&gt;Well not really... Up until this phase, we did not take the time to apply the &lt;strong&gt;juicy gaming theory&lt;/strong&gt; to our mini games to make them satisfying to play.&lt;/p&gt;

&lt;p&gt;We thought we were nearly ready to release but it finally took us &lt;strong&gt;several weeks of hard work&lt;/strong&gt; to make sure that on each and every gameplay interaction there was as much feedback as possible using sound design, FX effects, camera shakes and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7btv2ap0allwus8usl6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7btv2ap0allwus8usl6.gif" alt="Example of our juicy gaming design with FX effects, lights and so on" width="560" height="243"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;Example of our juicy gaming design with FX effects, lights and so on&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We also translated our game into &lt;strong&gt;multiple languages&lt;/strong&gt;, made it compatible with controllers, and added menus and notifications to improve accessibility which is &lt;strong&gt;not to be underestimated&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Now, let’s address the elephant in the room, &lt;strong&gt;the marketing&lt;/strong&gt;. As you probably realized, I did not talk about this up until now and that is clearly a mistake we made. We did &lt;strong&gt;not start marketing soon enough&lt;/strong&gt; and failed our release because nobody knew about the game. Basically we have learned that &lt;strong&gt;marketing should start as early as possible&lt;/strong&gt; because it takes time to build anticipation and a community from the ground up.&lt;/p&gt;

&lt;h1&gt;
  
  
  Failed launch
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;So... yeah, we failed our game release.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the most important fact is that we &lt;strong&gt;learned a bunch of things&lt;/strong&gt; and after working on it for countless hours, weekends and evenings, &lt;strong&gt;&lt;a href="https://store.steampowered.com/app/2889380/Gatherings" rel="noopener noreferrer"&gt;we actually released a real game on Steam&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;As 3 friends who were wondering if they could actually create a game from scratch during a random conversation about a year ago, I would say &lt;strong&gt;we did it&lt;/strong&gt;... And we are still friends!&lt;/p&gt;

&lt;p&gt;At the time of writing we sold &lt;strong&gt;240 units&lt;/strong&gt; that means we have managed to &lt;strong&gt;pay back our investment&lt;/strong&gt; of around 2000 euros in 3D assets, animations, music and sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key learnings along the way:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marketing is the hardest part&lt;/strong&gt; for us tech folks and it is really an important factor in the success of a game release. Fun fact, we leveraged some Reddit threads to improve &lt;a href="https://www.reddit.com/r/DestroyMyGame/comments/1fcpxmn/destroy_our_trailer/" rel="noopener noreferrer"&gt;our game trailer&lt;/a&gt; and &lt;a href="https://www.reddit.com/r/DestroyMySteamPage/comments/1fcpum8/what_is_wrong_with_our_steam_page_we_do_get_views/" rel="noopener noreferrer"&gt;texts&lt;/a&gt; for our Steam page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working with friends is not easy&lt;/strong&gt;, especially when relying on free time. Not everyone has the same amount of free time to give, and managing schedules is a hard task. Sometimes you will have to disagree and commit. Teamwork, adaptability and project management are a must.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gather and listen to feedback as much as you can and as early as possible&lt;/strong&gt;: We actually asked a lot of our friends for their honest feedback from start to finish and it was really valuable. When working daily on the game, we often lacked perspective and it helped us get back on track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating a game requires a dozen different skills&lt;/strong&gt;. We did not realize that before trying to make our own. Game design, sound design, FX effects, animations, UI/UX, 3D modeling, mapping, developing, QA, marketing... All of those are different jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The learning curve for Unreal Engine is steep&lt;/strong&gt;, but with dedication and a focus on mastering the basics, it's absolutely manageable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steam is taking 30% on each sale&lt;/strong&gt;, adding that to the bank fees and taxes, you need to sell a lot of units to gain real money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time is the most valuable resource&lt;/strong&gt; you have as an indie developer, spend it wisely on subjects that matter the most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unreal Engines Blueprint system is handy&lt;/strong&gt; but it will slow down a developer's pace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developing a multiplayer game&lt;/strong&gt; (even more for 8 people) is significantly more challenging than creating a single-player game. In hindsight, we probably should have started with a single-player project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you do if you had a year to create a video game? I’d love to see your ideas in the comments !&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>product</category>
      <category>nocode</category>
      <category>learning</category>
    </item>
    <item>
      <title>How we unified our Terraform module repositories</title>
      <dc:creator>Tom Masson</dc:creator>
      <pubDate>Fri, 14 Nov 2025 10:54:34 +0000</pubDate>
      <link>https://dev.to/tom-masson/nx-monorepo-how-we-unified-our-terraform-module-repositories-2h47</link>
      <guid>https://dev.to/tom-masson/nx-monorepo-how-we-unified-our-terraform-module-repositories-2h47</guid>
      <description>&lt;p&gt;Ever tried managing 15+ separate GitHub repositories for your Terraform modules? That's pretty much what we faced at Payfit. Our infrastructure codebase had fragmented into a nightmare of individual repos with time, each with its own CI/CD pipeline, versioning scheme, and tooling setup. Cross-module changes became coordination nightmares.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: When repositories multiply like rabbits
&lt;/h2&gt;

&lt;p&gt;Picture this: Your infrastructure team maintains multiple GitHub repos, with each Terraform module kept in its own repository. A simple VPC module change requires you to: update the module repo, bump versions in 5 downstream repos, trigger 6 separate CI pipelines, and spend the day merging PRs.&lt;/p&gt;

&lt;p&gt;That was us six months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our initial approach, a scaling nightmare
&lt;/h2&gt;

&lt;p&gt;Each Terraform module lived in its own repository with dedicated CI pipelines, separate versioning, and isolated tooling. While this provided clear ownership boundaries, it created massive overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version management hell &amp;amp; poor velocity&lt;/strong&gt;: Bumping versions across dependent modules required synchronized releases and multiple PRs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling drift&lt;/strong&gt;: Different tflint versions and terraform standards per repository&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context switching&lt;/strong&gt;: Teams bounced between repositories constantly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The breakthrough: Nx monorepo consolidation
&lt;/h2&gt;

&lt;p&gt;The turning point came when we consolidated all Terraform modules into a single monorepo. &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;Nx&lt;/a&gt; provided the orchestration layer that made this transformation not just possible, but elegant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project structure transformed
&lt;/h3&gt;

&lt;p&gt;Each Terraform module becomes an Nx library project under &lt;code&gt;terraform-modules/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-modules/
├── aws-lambda/        # Nx project: terraform-aws-lambda
├── aws-ecr/          # Nx project: terraform-aws-ecr
└── aws-s3/           # Nx project: terraform-aws-s3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unified module management
&lt;/h3&gt;

&lt;p&gt;Every Terraform module becomes a first-class Nx project with inferred tasks. Our internal local Nx plugin automatically detects terraform modules and provides standardized targets using &lt;a href="https://nx.dev/docs/concepts/inferred-tasks" rel="noopener noreferrer"&gt;Nx's inference system&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tofu-format&lt;/code&gt;: Consistent formatting across all modules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lint&lt;/code&gt;: Parallel linting with TFlint and validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu-format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nx:run-commands&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{projectRoot}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu fmt -recursive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;configurations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-write&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;defaultConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;technologies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Format OpenTofu code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nx:run-commands&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{projectRoot}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu init -backend=false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tflint --init&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tflint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu validate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;TFLINT_CONFIG_FILE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tflintConfigFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;technologies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tofu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lint OpenTofu code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The beauty?&lt;/strong&gt; Zero configuration needed. Drop a terraform module in the repo, and Nx handles everything automatically. We will even be able to generate the skeleton of a classic module using Nx's &lt;a href="https://nx.dev/docs/extending-nx/local-generators" rel="noopener noreferrer"&gt;generator system&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nx Release: unified module publishing
&lt;/h3&gt;

&lt;p&gt;The real transformation happened with &lt;a href="https://nx.dev/docs/features/manage-releases" rel="noopener noreferrer"&gt;Nx release&lt;/a&gt;. Our internal &lt;code&gt;@payfit/nx-core&lt;/code&gt; plugin orchestrates coordinated releases across all publishable artifacts over @ Payfit, including terraform modules.&lt;/p&gt;

&lt;p&gt;The release command analyzes git history and only releases modules that have actually changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nx release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The result?&lt;/strong&gt; Cross-module changes deploy together, while individual module updates release independently, in a single PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trade-offs: nothing's perfect
&lt;/h3&gt;

&lt;p&gt;Now, this monorepo approach isn't without its (small) downsides:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pro&lt;/th&gt;
&lt;th&gt;Con&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Smart Releases&lt;/strong&gt;: Intelligent orchestration across modules&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Larger Repository&lt;/strong&gt;: Single repo grows over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Parallel Execution&lt;/strong&gt;: Quality checks run simultaneously&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Nx Learning Curve&lt;/strong&gt;: Team needs to understand Nx concepts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Automated Standards&lt;/strong&gt;: Consistent tooling across all modules&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Migration Effort&lt;/strong&gt;: Consolidating 15 repos requires planning &amp;amp; time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For us, the benefits far outweigh these costs. &lt;strong&gt;We've dramatically improved our infrastructure velocity and consistency.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcomes: from chaos to streamlined Terraform modules management
&lt;/h2&gt;

&lt;p&gt;The impact was immediate and measurable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified Management&lt;/strong&gt;: Single Nx workspace orchestrates all 15+ modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Configuration&lt;/strong&gt;: Plugin automatically provides all terraform targets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Standards&lt;/strong&gt;: Consistent tooling and validation across all modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Velocity&lt;/strong&gt;: Single PR needed to update one or several modules simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight? &lt;strong&gt;Treat infrastructure modules like any other software project&lt;/strong&gt;, with proper versioning, testing, and release management. Nx makes this possible at scale.&lt;/p&gt;

&lt;p&gt;Have you faced similar repository chaos? How did you tackle it?&lt;/p&gt;

</description>
      <category>nx</category>
      <category>terraform</category>
      <category>infrastructureascode</category>
      <category>monorepo</category>
    </item>
    <item>
      <title>Automated frontend previews: Payfit’s path to faster PR reviews</title>
      <dc:creator>Tom Masson</dc:creator>
      <pubDate>Tue, 30 Sep 2025 07:55:43 +0000</pubDate>
      <link>https://dev.to/tom-masson/automated-frontend-previews-payfits-path-to-faster-pr-reviews-33mg</link>
      <guid>https://dev.to/tom-masson/automated-frontend-previews-payfits-path-to-faster-pr-reviews-33mg</guid>
      <description>&lt;h2&gt;
  
  
  The Bottleneck: Slow PR reviews
&lt;/h2&gt;

&lt;p&gt;📷 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?&lt;/p&gt;

&lt;p&gt;That was us moments ago. Our review process looked something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developer opens a PR&lt;/li&gt;
&lt;li&gt;The team reads the code (🤔 "This looks right...")&lt;/li&gt;
&lt;li&gt;They either approve based on code alone or ask for screenshots&lt;/li&gt;
&lt;li&gt;If screenshots aren't enough, well... We had to make it work somehow.&lt;/li&gt;
&lt;li&gt;Repeat until everyone's happy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The result?&lt;/strong&gt; Review cycles that took days instead of hours, and not-so-great velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our first attempt: Front-on-Demand and why it didn't scale
&lt;/h2&gt;

&lt;p&gt;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?).&lt;/p&gt;

&lt;p&gt;This worked... sort of. The problems quickly became apparent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sequential bottleneck&lt;/strong&gt;: Only one branch could be deployed at a time per team&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual overhead&lt;/strong&gt;: Devs had to remember how to deploy from their laptops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource waste&lt;/strong&gt;: Staging environments sitting idle or forgotten&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed something better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough: PR Previews on Autopilot
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's the workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1765ewfkf7cz6nu9fqze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1765ewfkf7cz6nu9fqze.png" alt="PR workflow" width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;Nx&lt;/a&gt; magic: our publish-preview executor
&lt;/h2&gt;

&lt;p&gt;Instead of writing custom deployment scripts for each app, we created a single, configurable &lt;a href="https://nx.dev/docs/concepts/executors-and-configurations" rel="noopener noreferrer"&gt;Nx executor&lt;/a&gt; that works across our entire mono-repo &amp;amp; easy to enroll for new apps. It handles the entire preview deployment pipeline based on simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Determine current branch (default or not)&lt;/li&gt;
&lt;li&gt;Get the current PR&lt;/li&gt;
&lt;li&gt;Get the built app and upload it to S3 using the AWS CLI&lt;/li&gt;
&lt;li&gt;Comment the PR to let the user know we've deployed/updated the preview environment of the PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Nx brings to the table
&lt;/h2&gt;

&lt;p&gt;Before we dive into the AWS infrastructure, let's pause and appreciate what Nx brought to the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🏗️ Monorepo Architecture&lt;/strong&gt;: Nx understands dependencies between apps and libraries, making it perfect for complex monorepos where changes ripple across multiple applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Affected Project Detection&lt;/strong&gt;: Nx automatically identifies which apps are impacted by your changes. No more manual configuration or complicated guessing, Nx figures it out for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔧 Executor System&lt;/strong&gt;: 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 &lt;code&gt;"previewDeploy": "true"&lt;/code&gt; in the &lt;code&gt;project.json&lt;/code&gt; file and you’re basically done !&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To power all of this, we run a single command in CI: &lt;code&gt;yarn nx affected -t publish-preview&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Infra magic with AWS: S3 + CloudFront + Lambda@Edge
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5mykvoqzflx20alm1la.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5mykvoqzflx20alm1la.png" alt="AWS Infrastructure" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our Nx executor uploads each PR's build to a specific S3 path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For each PR, each affected app gets its own S3 path: {pr-number}-{app-name}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`s3://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="c1"&gt;// Upload with no-cache headers for immediate updates&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`aws s3 sync &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;distPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s3Path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --cache-control "max-age=0"`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, thanks to both our lambdas at edge we secure the CloudFront endpoint (which is public by default) &amp;amp; rewrite the path to be able to use clean URLs like &lt;code&gt;https://{pr-number}-{app-name}.preview.my-domain.com&lt;/code&gt;. The path rewrite is looking something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing subdomain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR #123 for app foo → &lt;code&gt;https://123-foo.preview.my-domain.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PR #456 for app bar → &lt;code&gt;https://456-bar.preview.my-domain.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our Lambda@Edge function routes these custom domains to the correct S3 paths.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8c2u64q6q4q1dfiybtbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8c2u64q6q4q1dfiybtbw.png" alt="Comment on PR" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcomes: Faster Reviews, Happier Developers 📈
&lt;/h2&gt;

&lt;p&gt;The impact was immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🕐 Review time&lt;/strong&gt;: while we weren’t able to measure it precisely, it dropped significantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💪 Confidence&lt;/strong&gt;: Reviewers could actually see and test changes in a couple of minutes tops !&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the best metric? Developer satisfaction. No more "Can you deploy this so I can see it? Is the environment free?" messages in Slack.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key was treating previews and developer experience not as an afterthought, but as a core part of our development workflow.&lt;/strong&gt; 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 &lt;strong&gt;less than 2 minutes to deploy an up-to-date app&lt;/strong&gt;. And there is nearly 0 maintenance time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next: Towards a Developer Platform
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;nx release&lt;/code&gt;, which will be the subject of another blog post pretty soon!&lt;/p&gt;

&lt;p&gt;👂 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?&lt;/p&gt;

</description>
      <category>nx</category>
      <category>devops</category>
      <category>devex</category>
      <category>idp</category>
    </item>
  </channel>
</rss>
