<?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: Natalia</title>
    <description>The latest articles on DEV Community by Natalia (@natalia_c8ace1e6703e8f29f).</description>
    <link>https://dev.to/natalia_c8ace1e6703e8f29f</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%2F3957664%2F472b4272-2206-436a-9a5a-e1318a9616f4.png</url>
      <title>DEV Community: Natalia</title>
      <link>https://dev.to/natalia_c8ace1e6703e8f29f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/natalia_c8ace1e6703e8f29f"/>
    <language>en</language>
    <item>
      <title>Notes on Configuring TanStack Start for Cloudflare Workers</title>
      <dc:creator>Natalia</dc:creator>
      <pubDate>Thu, 04 Jun 2026 08:11:55 +0000</pubDate>
      <link>https://dev.to/natalia_c8ace1e6703e8f29f/notes-on-configuring-tanstack-start-for-cloudflare-workers-192n</link>
      <guid>https://dev.to/natalia_c8ace1e6703e8f29f/notes-on-configuring-tanstack-start-for-cloudflare-workers-192n</guid>
      <description>&lt;p&gt;I like frameworks that make the boring parts stay boring.&lt;/p&gt;

&lt;p&gt;For an AI image website like &lt;a href="https://flux2pro.org/" rel="noopener noreferrer"&gt;Flux 2&lt;/a&gt;, the frontend is only one slice of the work. There are landing pages, account flows, uploads, generation history, admin screens, and a lot of small server-side decisions around performance and reliability. That is why TanStack Start on Cloudflare Workers is interesting: Vite-based development, full-stack routing, server functions, static assets, and an edge runtime can all fit into one deployment story.&lt;/p&gt;

&lt;p&gt;The part I care about most is not the headline feature list. It is the config.&lt;/p&gt;

&lt;p&gt;If the config is clear, the team knows what gets deployed, where it runs, and which runtime assumptions the app is making. If the config is messy, every release feels a little suspicious.&lt;/p&gt;

&lt;p&gt;This is the short version of how I would approach a TanStack Start + Cloudflare setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Cloudflare target in mind
&lt;/h2&gt;

&lt;p&gt;TanStack Start can run on different hosting targets, but Cloudflare Workers has a specific shape. The official &lt;a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/" rel="noopener noreferrer"&gt;Cloudflare TanStack Start guide&lt;/a&gt; shows two useful paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a new app already configured for Cloudflare&lt;/li&gt;
&lt;li&gt;adapt an existing TanStack Start app by adding Wrangler and the Cloudflare Vite plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a new project, I would rather start with the Cloudflare path than retrofit it later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm create cloudflare@latest my-app &lt;span class="nt"&gt;--framework&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tanstack-start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an existing project, the main idea is simple: TanStack Start still uses Vite, but the Cloudflare plugin needs to participate in the server-side build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vite config is the first file I check
&lt;/h2&gt;

&lt;p&gt;The Vite config tells me whether the project is actually being built for the runtime I expect.&lt;/p&gt;

&lt;p&gt;A Cloudflare-oriented setup usually looks 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cloudflare&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@cloudflare/vite-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tanstackStart&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-start/plugin/vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;cloudflare&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;viteEnvironment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ssr&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="nf"&gt;tanstackStart&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;react&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;The important part is not memorizing every line. The important part is that the SSR environment is intentionally wired through Cloudflare. That keeps the server side of the app close to the Worker runtime instead of accidentally drifting toward a generic Node.js assumption.&lt;/p&gt;

&lt;p&gt;That matters when the app grows. Server functions, route loaders, auth checks, media metadata, and dashboard requests are all easier to reason about when the runtime is explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrangler config should be small at first
&lt;/h2&gt;

&lt;p&gt;The next file I look at is &lt;code&gt;wrangler.jsonc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I prefer starting small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node_modules/wrangler/config-schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-tanstack-start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@tanstack/react-start/server-entry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does a few useful things.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main&lt;/code&gt; points Wrangler at the TanStack Start server entry. &lt;code&gt;compatibility_date&lt;/code&gt; makes the Workers runtime version explicit. &lt;code&gt;nodejs_compat&lt;/code&gt; gives the app a more practical compatibility baseline for packages that expect some Node APIs. &lt;code&gt;observability&lt;/code&gt; makes the first round of debugging less blind.&lt;/p&gt;

&lt;p&gt;I do not like stuffing this file with every future binding on day one. The config should grow when the product needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add scripts that explain the workflow
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;package.json&lt;/code&gt; scripts should make the common path obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm build &amp;amp;&amp;amp; wrangler deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cf-typegen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler types"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this because it separates concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dev&lt;/code&gt; is for local development&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build&lt;/code&gt; proves the app can compile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preview&lt;/code&gt; lets me inspect the built result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deploy&lt;/code&gt; keeps the build step attached to release&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cf-typegen&lt;/code&gt; keeps Worker bindings typed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is easy to skip, but it is useful once bindings enter the project. A typed &lt;code&gt;env&lt;/code&gt; is much better than guessing whether a binding name is available in a server function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat bindings as product decisions
&lt;/h2&gt;

&lt;p&gt;For a real AI image product, Cloudflare bindings can become important quickly.&lt;/p&gt;

&lt;p&gt;You might use R2 for generated media, D1 for lightweight relational data, KV for low-risk cache data, Queues for background jobs, or a service binding for splitting a larger backend into smaller Workers.&lt;/p&gt;

&lt;p&gt;But I try not to add bindings just because the platform supports them. I add them when a workflow needs a clear ownership boundary.&lt;/p&gt;

&lt;p&gt;For example, an upload or generated asset flow may justify an R2 binding. A scheduled cleanup job may justify a custom entrypoint. A separate auth or billing service may justify a service binding. The config should tell that story plainly.&lt;/p&gt;

&lt;p&gt;Inside TanStack Start server code, Cloudflare bindings are accessed through the Worker environment. That is a good fit for server functions because browser code does not need to know about storage buckets, queues, or internal services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be careful with prerendering
&lt;/h2&gt;

&lt;p&gt;TanStack Start can prerender routes, and Cloudflare can serve static assets efficiently. That is useful for pages that do not depend on per-user state.&lt;/p&gt;

&lt;p&gt;I would consider prerendering for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;marketing pages&lt;/li&gt;
&lt;li&gt;documentation-style pages&lt;/li&gt;
&lt;li&gt;static comparison pages&lt;/li&gt;
&lt;li&gt;content that changes on a predictable schedule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not use it blindly for account pages, generation history, payment status, or anything that depends on a signed-in user.&lt;/p&gt;

&lt;p&gt;The trap is not prerendering itself. The trap is forgetting when data is read. Build-time data and request-time data are different. Once that distinction is clear, prerendering becomes a useful tool instead of a source of confusing stale pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep environments boring
&lt;/h2&gt;

&lt;p&gt;I want staging and production to be boringly explicit.&lt;/p&gt;

&lt;p&gt;That usually means separate Worker names and routes, with the same general structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-tanstack-start-staging"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-tanstack-start-production"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then commands become hard to misread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm build
wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; staging &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; staging
wrangler &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; staging

wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; production &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; production
wrangler &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tail&lt;/code&gt; step is not glamorous, but it is one of the fastest ways to notice obvious runtime mistakes after a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config is part of the product
&lt;/h2&gt;

&lt;p&gt;For a TanStack Start app on Cloudflare, I think of configuration as product infrastructure, not boilerplate.&lt;/p&gt;

&lt;p&gt;The Vite config says how the app is built. The Wrangler config says how it runs. The scripts say how developers interact with it. Bindings say which Cloudflare resources the app depends on. Environment sections say where a release is going.&lt;/p&gt;

&lt;p&gt;That is why I prefer a small, readable setup over a clever one.&lt;/p&gt;

&lt;p&gt;For an AI image website, the product can be complex enough already. The deployment path should be easy to inspect: TanStack Start for the app shape, Cloudflare Workers for the runtime, Wrangler for the release workflow, and only the bindings the product actually needs.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why AI Image Generation Should Be Async</title>
      <dc:creator>Natalia</dc:creator>
      <pubDate>Fri, 29 May 2026 04:33:18 +0000</pubDate>
      <link>https://dev.to/natalia_c8ace1e6703e8f29f/why-ai-image-generation-should-be-async-k81</link>
      <guid>https://dev.to/natalia_c8ace1e6703e8f29f/why-ai-image-generation-should-be-async-k81</guid>
      <description>&lt;p&gt;AI image generation can look like a simple request-response feature.&lt;/p&gt;

&lt;p&gt;A user enters a prompt, clicks generate, and waits for an image.&lt;/p&gt;

&lt;p&gt;For a prototype, that can work. For a production product, it usually becomes fragile.&lt;/p&gt;

&lt;p&gt;Image generation may take several seconds or minutes. A provider may return a job ID first and the final result later. Some results arrive through webhooks. Others need polling. Requests can fail, time out, or finish after the user has already left the page.&lt;/p&gt;

&lt;p&gt;That is why AI image generation is usually better designed as an asynchronous workflow.&lt;/p&gt;

&lt;p&gt;This is the approach I use while building &lt;a href="https://imagev2.org/" rel="noopener noreferrer"&gt;Image 2&lt;/a&gt;, a multi-model AI image generation and editing platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simple Version
&lt;/h2&gt;

&lt;p&gt;The most direct implementation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User -&amp;gt; API route -&amp;gt; AI provider -&amp;gt; result -&amp;gt; user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easy to understand, but it has several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the HTTP request may time out&lt;/li&gt;
&lt;li&gt;retries can create duplicate jobs&lt;/li&gt;
&lt;li&gt;the frontend depends on provider latency&lt;/li&gt;
&lt;li&gt;billing or credit logic becomes harder to protect&lt;/li&gt;
&lt;li&gt;generated media may live on temporary provider URLs&lt;/li&gt;
&lt;li&gt;failures are difficult to repair after the request ends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is fine for demos. It is not ideal once real users, payments, storage, and retries are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Shape
&lt;/h2&gt;

&lt;p&gt;A more reliable version separates the user request from the generation work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User request
  |
  v
Create generation record
  |
  v
Push message to queue
  |
  v
Background worker submits job
  |
  v
Webhook or polling gets result
  |
  v
Store asset and update status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user-facing request returns quickly after creating the task. The UI can then show a status such as &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, or &lt;code&gt;failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The slow work happens in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Async Helps
&lt;/h2&gt;

&lt;p&gt;Async generation gives the system more room to recover.&lt;/p&gt;

&lt;p&gt;If the provider is slow, the task can remain in &lt;code&gt;processing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the provider fails, the system can mark the task as failed and roll back credits.&lt;/p&gt;

&lt;p&gt;If a webhook is missed, a scheduled job can poll the provider later.&lt;/p&gt;

&lt;p&gt;If both a webhook and a polling job see the same final result, the system can ignore duplicate settlement.&lt;/p&gt;

&lt;p&gt;That last point matters. In production, the same generation result may be observed more than once. Final states such as &lt;code&gt;completed&lt;/code&gt; and &lt;code&gt;failed&lt;/code&gt; should be idempotent.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small State Model
&lt;/h2&gt;

&lt;p&gt;You do not need a complicated state machine to start. A simple model is often enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created -&amp;gt; queued -&amp;gt; processing -&amp;gt; completed
                         |
                         -&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state should mean something clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;created&lt;/code&gt;: the request was accepted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queued&lt;/code&gt;: background work has been scheduled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processing&lt;/code&gt;: the provider job has started&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;completed&lt;/code&gt;: the final asset is available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: the task cannot complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important rule is that terminal states should be protected. Once a task is completed or failed, retries and duplicate callbacks should not apply the same result again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the Result Yourself
&lt;/h2&gt;

&lt;p&gt;Many AI providers return a URL for the generated image. That URL may be temporary or provider-controlled.&lt;/p&gt;

&lt;p&gt;For a real product, it is often safer to copy the result into your own storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider result URL -&amp;gt; app storage -&amp;gt; stable asset URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Cloudflare, that might mean storing the final image in R2 and serving it from your own CDN domain.&lt;/p&gt;

&lt;p&gt;This makes future product behavior easier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user ownership checks&lt;/li&gt;
&lt;li&gt;downloads&lt;/li&gt;
&lt;li&gt;cleanup&lt;/li&gt;
&lt;li&gt;moderation&lt;/li&gt;
&lt;li&gt;stable previews&lt;/li&gt;
&lt;li&gt;billing history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI provider creates the image. Your application should own the product workflow around that image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Multi-Model Apps Get More Complex
&lt;/h2&gt;

&lt;p&gt;Async workflows become even more useful when an app supports more than one model or generation style.&lt;/p&gt;

&lt;p&gt;A text-to-image model, an image editing model, and a reference-image workflow may all behave differently. Some may return results quickly. Others may need a provider-side job ID. Some may support high-resolution output. Some may have different input limits.&lt;/p&gt;

&lt;p&gt;A product like &lt;a href="https://imagev2.org/" rel="noopener noreferrer"&gt;Image 2&lt;/a&gt; can expose those workflows through a simpler user interface while keeping provider-specific details in the backend. For example, separate pages such as the &lt;a href="https://imagev2.org/image/gpt-images-2" rel="noopener noreferrer"&gt;GPT Images 2.0 image generator&lt;/a&gt; or the &lt;a href="https://imagev2.org/image/nano-banana-2" rel="noopener noreferrer"&gt;Nano Banana 2 AI image generator&lt;/a&gt; can still share the same general task lifecycle.&lt;/p&gt;

&lt;p&gt;That is the main benefit of designing around the workflow instead of designing around one provider API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;AI image generation is not just a model call. It is a product workflow.&lt;/p&gt;

&lt;p&gt;For experiments, a synchronous API route is enough. For production, async architecture gives you a cleaner way to handle slow jobs, duplicate callbacks, retries, storage, moderation, and credit accounting.&lt;/p&gt;

&lt;p&gt;The model creates the image. The workflow makes the product reliable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
