<?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: Thomas Plat</title>
    <description>The latest articles on DEV Community by Thomas Plat (@thpl).</description>
    <link>https://dev.to/thpl</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3965848%2Fb056b44e-053e-4a2d-93c3-dea5c162dd61.jpg</url>
      <title>DEV Community: Thomas Plat</title>
      <link>https://dev.to/thpl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thpl"/>
    <language>en</language>
    <item>
      <title>Deploying a Complex Cursor-Built App: Prisma, Postgres, Inngest &amp; Supabase on Vercel, Railway and Jetpacked</title>
      <dc:creator>Thomas Plat</dc:creator>
      <pubDate>Sun, 07 Jun 2026 11:09:42 +0000</pubDate>
      <link>https://dev.to/thpl/deploying-a-complex-cursor-built-app-prisma-postgres-inngest-supabase-in-production-4019</link>
      <guid>https://dev.to/thpl/deploying-a-complex-cursor-built-app-prisma-postgres-inngest-supabase-in-production-4019</guid>
      <description>&lt;p&gt;I built a complex full-stack application with Cursor to see how different deployment platforms handle realistic AI-generated code.&lt;/p&gt;

&lt;p&gt;Not a todo app. Not a landing page. Not a clean demo with one framework and one environment variable. I wanted something closer to what people actually end up building when they spend a few evenings with Cursor, Claude, ChatGPT, or any of the newer agentic coding tools: a product-shaped app with auth, a database, background work, third-party APIs, and enough moving parts that deployment stops being a simple &lt;code&gt;npm run build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result was &lt;strong&gt;VibeSplit&lt;/strong&gt;, a group expense splitter with AI receipt scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test App
&lt;/h2&gt;

&lt;p&gt;VibeSplit lets a group upload receipts, parse them with AI, split expenses, track balances, invite other people, and settle up. It is intentionally the kind of app that feels easy to create with an AI coding assistant and then suddenly becomes much harder when it has to run somewhere real.&lt;/p&gt;

&lt;p&gt;The stack looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15 with the App Router and TypeScript&lt;/li&gt;
&lt;li&gt;Prisma with PostgreSQL&lt;/li&gt;
&lt;li&gt;Supabase Auth for magic links and Google login&lt;/li&gt;
&lt;li&gt;Inngest for background jobs&lt;/li&gt;
&lt;li&gt;OpenAI Vision for receipt parsing&lt;/li&gt;
&lt;li&gt;Stripe for settlement payments&lt;/li&gt;
&lt;li&gt;Resend for application emails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the shape of many vibe-coded apps I see: the code works locally, but production needs a real database, real secrets, OAuth redirect URLs, webhook URLs, background jobs, migrations, and build-time public environment variables.&lt;/p&gt;

&lt;p&gt;That last phrase matters more than it sounds. Some variables are runtime-only. Others, like &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt;, are baked into the browser bundle during the Next.js build. If a platform passes environment variables only when the container starts, the backend may be fine while the frontend quietly ships with placeholder values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Was A Useful Test
&lt;/h2&gt;

&lt;p&gt;Simple apps hide deployment problems. A static frontend can make almost any platform look good. A basic Next.js app with no database and no integrations usually deploys without much drama.&lt;/p&gt;

&lt;p&gt;VibeSplit surfaced the messier parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma needs a valid &lt;code&gt;DATABASE_URL&lt;/code&gt; and production migrations.&lt;/li&gt;
&lt;li&gt;Supabase Auth needs public client values at build time and redirect URLs after deploy.&lt;/li&gt;
&lt;li&gt;Stripe needs callback URLs that point to the live app.&lt;/li&gt;
&lt;li&gt;Inngest needs a reachable endpoint for background jobs.&lt;/li&gt;
&lt;li&gt;OpenAI and Resend need server-side secrets.&lt;/li&gt;
&lt;li&gt;The app needs to bind to the correct port inside a container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was not to prove that any platform is bad. The goal was to see which platform understood the shape of the app without making me manually reconstruct the architecture in a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Railway
&lt;/h2&gt;

&lt;p&gt;Railway handled the Next.js part reasonably well at first. It detected that the app was a Node/Next project and tried to run it in the expected way.&lt;/p&gt;

&lt;p&gt;The first problem was the port. The initial deployment came up with a 502 error because the app and the platform did not agree on the port. This was resolved fairly quickly, but it is exactly the kind of issue that breaks the illusion of one-click deploy. The app was alive somewhere, but traffic was not reaching it correctly.&lt;/p&gt;

&lt;p&gt;After the port issue, the bigger problem was the database. Railway did not provision a PostgreSQL database automatically for this repo. The app used Prisma, had a Prisma schema, and clearly needed a database, but I still had to add Postgres manually.&lt;/p&gt;

&lt;p&gt;That meant I had to wire &lt;code&gt;DATABASE_URL&lt;/code&gt;, think about Prisma migrations, and check whether the runtime environment matched what the app expected. Even after adding Postgres, I still ran into internal server errors. At that point the deployment had moved from "deploy my repo" to "debug platform configuration, application assumptions, and service wiring at the same time."&lt;/p&gt;

&lt;p&gt;Railway is powerful once you know what you are doing, but for this kind of AI-generated full-stack app, it still expected too much manual reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vercel
&lt;/h2&gt;

&lt;p&gt;Vercel deployed the frontend successfully. That is not surprising; Vercel is very good at Next.js.&lt;/p&gt;

&lt;p&gt;But the live site returned a 500 Internal Server Error. The frontend build existing was not enough. This app was not just a frontend. It needed a database, Prisma, migrations, Supabase values, API routes, server actions, and several third-party secrets.&lt;/p&gt;

&lt;p&gt;The tricky part with Vercel is that a successful deployment can look deceptively complete. The build passes, a URL exists, and the app is technically online. But the first real user flow reveals what was missing.&lt;/p&gt;

&lt;p&gt;In this case, the platform did not infer the full backend shape of the application. I still had to think through the database and environment setup myself. For a clean Next.js app, that is fine. For a multi-service vibe-coded app, the gap is noticeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jetpacked
&lt;/h2&gt;

&lt;p&gt;Jetpacked detected the app as a full-stack deployment instead of treating it as just a Next.js frontend.&lt;/p&gt;

&lt;p&gt;It picked up the Postgres requirement, the Prisma setup, and the required environment variables. That made the deployment feel closer to the way I want this category of platform to work: read the repo, understand the stack, then ask me for the values that cannot be inferred.&lt;/p&gt;

&lt;p&gt;The deployment completed with the least friction of the three. I did not have to manually create a database first and then wire the app around it. The platform understood that the database was part of the application shape.&lt;/p&gt;

&lt;p&gt;That said, it was not magic. A few things still required manual attention.&lt;/p&gt;

&lt;p&gt;Supabase Auth redirect URLs had to be configured in the Supabase dashboard after deployment. This is easy to forget. The app can deploy perfectly and still fail login if Supabase only allows &lt;code&gt;localhost:3000&lt;/code&gt; as a redirect target.&lt;/p&gt;

&lt;p&gt;I also ran into the difference between runtime and build-time environment variables. For Next.js, public variables like &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt; must be available during the build or the frontend bundle can contain placeholder values from &lt;code&gt;.env.example&lt;/code&gt;. That is a subtle production issue because the container can have the correct runtime env while the browser is already running code built with the wrong value.&lt;/p&gt;

&lt;p&gt;This is the kind of issue that deployment tooling needs to understand. It is not enough to inject env vars into the running container. For frameworks like Next.js, some of them must also be passed into the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase Was Its Own Step
&lt;/h2&gt;

&lt;p&gt;Supabase Auth introduced a separate class of deployment work.&lt;/p&gt;

&lt;p&gt;The app used Supabase for magic links and Google login. The environment variables pointed the app to the correct Supabase project, but Supabase itself still needed production URL configuration.&lt;/p&gt;

&lt;p&gt;The Google provider had to be enabled in Supabase. The deployed callback URL had to be added. The site URL had to stop pointing at localhost. None of that lives in the application repo, and no hosting platform can fully infer it from code.&lt;/p&gt;

&lt;p&gt;This is an important lesson for vibe coders: when your app uses external auth, deployment is not only about your server. It is also about configuring the provider to trust the new production URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Complex vibe-coded apps need platforms that understand more than framework names.&lt;/p&gt;

&lt;p&gt;Detecting "Next.js" is useful, but it is not enough. The real deployment shape is Next.js plus Prisma plus Postgres plus migrations plus Supabase plus background jobs plus public build-time env vars plus server-only secrets.&lt;/p&gt;

&lt;p&gt;Railway got me part of the way there but required manual service setup and port debugging. Vercel handled the frontend but did not solve the full-stack requirements by itself. Jetpacked handled the complete setup noticeably smoother for getting the app live quickly.&lt;/p&gt;

&lt;p&gt;The rough edges were instructive. Supabase still needs manual provider and redirect configuration. OAuth still depends on external dashboards. Build-time environment variables still matter. But the core deployment path was much closer to what I want: connect the repo, detect the real stack, provision what the app needs, and surface the remaining configuration clearly.&lt;/p&gt;

&lt;p&gt;That is the bar deployment platforms need to clear for AI-generated applications. Not "can it build a Next.js app?" but "can it understand the messy, service-heavy thing someone actually built?"&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>vibecoding</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Container Port Binding Mistake That Breaks Almost Every First Deploy</title>
      <dc:creator>Thomas Plat</dc:creator>
      <pubDate>Wed, 03 Jun 2026 06:40:17 +0000</pubDate>
      <link>https://dev.to/thpl/the-container-port-binding-mistake-that-breaks-almost-every-first-deploy-3fng</link>
      <guid>https://dev.to/thpl/the-container-port-binding-mistake-that-breaks-almost-every-first-deploy-3fng</guid>
      <description>&lt;p&gt;You deploy your app. The build succeeds. The logs show the server starting. You click the URL your deployment platform gave you and get a connection error, a 502, or nothing at all.&lt;/p&gt;

&lt;p&gt;This is one of the most common first deployment failures, and the cause is almost always the same: the app is binding to the wrong address, or listening on the wrong port, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What port binding actually means
&lt;/h2&gt;

&lt;p&gt;When a server app starts, it listens for incoming connections on a network address. That address has two parts: the IP address it listens on, and the port number.&lt;/p&gt;

&lt;p&gt;The IP address determines which network interfaces the application accepts connections from. &lt;code&gt;localhost&lt;/code&gt; (which resolves to &lt;code&gt;127.0.0.1&lt;/code&gt;) means the app only accepts connections from the same machine. &lt;code&gt;0.0.0.0&lt;/code&gt; means the app accepts connections from any network interface, including external ones.&lt;/p&gt;

&lt;p&gt;During local development, &lt;code&gt;localhost&lt;/code&gt; is fine. Everything is on the same machine. Your browser and your server are both on your laptop. When you deploy to a server, the platform's load balancer is not on the same machine as your app. It is trying to connect from outside. An app bound to &lt;code&gt;localhost&lt;/code&gt; is invisible to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The port problem
&lt;/h2&gt;

&lt;p&gt;Deployment platforms often assign ports dynamically. They tell your app which port to use through an environment variable, almost always called &lt;code&gt;PORT&lt;/code&gt;. Your app needs to read this variable and bind to that port.&lt;/p&gt;

&lt;p&gt;If your app ignores &lt;code&gt;PORT&lt;/code&gt; and hardcodes a port number, it starts on a port the platform is not watching. The platform tries to connect on its assigned port, gets nothing, and marks the deployment as failed.&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="c1"&gt;// This will fail on most platforms&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// This is correct&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| 3000&lt;/code&gt; fallback makes the app work both locally (where &lt;code&gt;PORT&lt;/code&gt; is not set) and in production (where the platform sets it).&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI tools generate
&lt;/h2&gt;

&lt;p&gt;AI tools often hardcode both the address and the port. The generated code looks 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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server running on http://localhost:3000&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct for local development and wrong for production. The &lt;code&gt;'localhost'&lt;/code&gt; argument is the binding address. Remove it entirely or replace it with &lt;code&gt;'0.0.0.0'&lt;/code&gt;. Replace &lt;code&gt;3000&lt;/code&gt; with &lt;code&gt;process.env.PORT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same pattern appears across multiple frameworks. Express does it. Fastify does it. Hapi does it. The underlying behavior is the same in all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework-specific fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Express:&lt;/strong&gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&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;&lt;strong&gt;Fastify:&lt;/strong&gt;&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&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;&lt;strong&gt;AdonisJS:&lt;/strong&gt; Set &lt;code&gt;HOST=0.0.0.0&lt;/code&gt; and &lt;code&gt;PORT&lt;/code&gt; in your environment. AdonisJS reads both from environment variables automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js:&lt;/strong&gt; Next.js handles port binding correctly by default and reads &lt;code&gt;PORT&lt;/code&gt; from the environment. No manual fix needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NestJS:&lt;/strong&gt;&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&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;h2&gt;
  
  
  How to diagnose it
&lt;/h2&gt;

&lt;p&gt;If your deployment shows the application is starting but the health check is failing, check two things in the startup logs:&lt;/p&gt;

&lt;p&gt;First, what address is the server logging? If you see &lt;code&gt;Listening on http://localhost:3000&lt;/code&gt; or &lt;code&gt;Server running on 127.0.0.1:3000&lt;/code&gt;, the app is bound to localhost. External traffic cannot reach it.&lt;/p&gt;

&lt;p&gt;Second, what port is the app using? If it is hardcoded and does not match the &lt;code&gt;PORT&lt;/code&gt; environment variable, the platform is sending traffic to the wrong port.&lt;/p&gt;

&lt;p&gt;Both of these are visible in the startup log lines that most frameworks print when they start successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is so consistent
&lt;/h2&gt;

&lt;p&gt;This failure is nearly universal for first deployments because it is invisible during development. The hardcoded localhost binding works perfectly when you are testing locally. Nothing ever fails. The code ships, the app starts on the server, and the binding address becomes a problem for the first time.&lt;/p&gt;

&lt;p&gt;Catching this before deployment is one of the more valuable things a deployment platform can do automatically. jetpacked.ai detects hardcoded port and address bindings during repo analysis — apps that don't listen on 0.0.0.0 or ignore PORT won't serve traffic, and surfacing that before the build starts saves the debugging loop entirely.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
