<?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>Forem: Tom Masson</title>
    <description>The latest articles on Forem by Tom Masson (@tom-masson).</description>
    <link>https://forem.com/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>Forem: Tom Masson</title>
      <link>https://forem.com/tom-masson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tom-masson"/>
    <language>en</language>
    <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://forem.com/tom-masson/the-fruit-basket-problem-rebuilding-payfits-platform-trust-alignment-3ena</link>
      <guid>https://forem.com/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://forem.com/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://forem.com/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://forem.com/tom-masson/nx-monorepo-how-we-unified-our-terraform-module-repositories-2h47</link>
      <guid>https://forem.com/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://forem.com/tom-masson/automated-frontend-previews-payfits-path-to-faster-pr-reviews-33mg</link>
      <guid>https://forem.com/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>
