<?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>A Model Catalog Is the Missing Product Contract in Multi-Model AI Apps</title>
      <dc:creator>Natalia</dc:creator>
      <pubDate>Fri, 24 Jul 2026 04:14:46 +0000</pubDate>
      <link>https://dev.to/natalia_c8ace1e6703e8f29f/a-model-catalog-is-the-missing-product-contract-in-multi-model-ai-apps-2j4b</link>
      <guid>https://dev.to/natalia_c8ace1e6703e8f29f/a-model-catalog-is-the-missing-product-contract-in-multi-model-ai-apps-2j4b</guid>
      <description>&lt;p&gt;The first AI model in a product rarely creates an architecture problem.&lt;/p&gt;

&lt;p&gt;You build a form, call an API, and display a result.&lt;/p&gt;

&lt;p&gt;The second model adds a dropdown and a few conditions. The third adds another input mode. Before long, the product supports text-to-image, image-to-image, text-to-video, image-to-video, reference-based generation, video editing, background removal, and upscaling.&lt;/p&gt;

&lt;p&gt;At that point, calling the provider is no longer the hardest part.&lt;/p&gt;

&lt;p&gt;The difficult questions are product questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which modes does each model support?&lt;/li&gt;
&lt;li&gt;Which controls should the form render?&lt;/li&gt;
&lt;li&gt;How many reference assets can a user upload?&lt;/li&gt;
&lt;li&gt;Does the model accept images, video, audio, or a combination?&lt;/li&gt;
&lt;li&gt;Which parameter combinations are valid?&lt;/li&gt;
&lt;li&gt;How does resolution, duration, or source media affect the credit cost?&lt;/li&gt;
&lt;li&gt;Can the provider rename a model without breaking public URLs or stored tasks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While building &lt;a href="https://ai-img-editor.com/" rel="noopener noreferrer"&gt;AI Image Editor&lt;/a&gt;, I found that a provider adapter only solves part of this problem. The application also needs a model catalog above the adapter: one product-owned contract describing what every model means inside the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider Adapters Answer the Wrong Question
&lt;/h2&gt;

&lt;p&gt;A provider adapter is still useful. It can normalize operations such as:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GenerationProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProviderJob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProviderResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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 rest of the application does not need to care whether one provider calls an identifier &lt;code&gt;taskId&lt;/code&gt; and another calls it &lt;code&gt;predictionId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But an adapter answers, "How do I call this service?"&lt;/p&gt;

&lt;p&gt;It does not answer, "What should this product allow the user to do?"&lt;/p&gt;

&lt;p&gt;One provider can expose many models with completely different product rules. One image model may accept 16 reference images, while another accepts four. One video model may accept video input, while another only accepts a starting image. Some models charge by resolution. Others charge by output duration or by both input and output media.&lt;/p&gt;

&lt;p&gt;Those differences belong to the product layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe Capabilities as Data
&lt;/h2&gt;

&lt;p&gt;A useful catalog entry can look 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;type&lt;/span&gt; &lt;span class="nx"&gt;ModelCatalogEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;supportsReferenceAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;maxReferenceAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;sourceAssetAccept&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;creditCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreditCostTier&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;providerRoutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderRoute&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 object is not a copy of a provider response. It is the application's own definition of the model.&lt;/p&gt;

&lt;p&gt;The internal &lt;code&gt;id&lt;/code&gt; and public &lt;code&gt;slug&lt;/code&gt; belong to the product. A provider-specific model name belongs inside &lt;code&gt;providerRoutes&lt;/code&gt;. If an integration changes later, the application should not have to rewrite database history, public page URLs, and frontend state.&lt;/p&gt;

&lt;p&gt;Product identity should be stable. Provider identity should be replaceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the Form From Capabilities
&lt;/h2&gt;

&lt;p&gt;Without a catalog, model forms often become a growing collection of conditions:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model-a&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;showResolution&lt;/span&gt;&lt;span class="p"&gt;()&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;model&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model-c&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;allowReferenceImages&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 feels direct while the catalog is small. It becomes fragile when models are renamed, upgraded, or share only some capabilities.&lt;/p&gt;

&lt;p&gt;A capability-driven form reads the entry instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;modes&lt;/code&gt; renders text-to-image, image-to-image, or video-edit tabs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fields&lt;/code&gt; renders resolution, aspect ratio, duration, and audio controls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;defaults&lt;/code&gt; initializes the form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxReferenceAssets&lt;/code&gt; controls the uploader&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sourceAssetAccept&lt;/code&gt; limits media types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a model becomes primarily a data change, followed by contract tests, instead of a search for every component that knows the model name.&lt;/p&gt;

&lt;p&gt;The frontend also stays deliberately boring. It renders product capabilities without learning provider terminology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce the Same Contract on the Server
&lt;/h2&gt;

&lt;p&gt;Generating the UI from a catalog is not enough. If the server does not use the same rules, the catalog is display configuration rather than a contract.&lt;/p&gt;

&lt;p&gt;After receiving a request, the server should check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model exists and matches the requested media kind.&lt;/li&gt;
&lt;li&gt;The requested mode belongs to the model.&lt;/li&gt;
&lt;li&gt;The selected mode accepts the supplied references.&lt;/li&gt;
&lt;li&gt;The number and kinds of assets are allowed.&lt;/li&gt;
&lt;li&gt;Dynamic fields contain only declared values.&lt;/li&gt;
&lt;li&gt;Cross-field combinations are compatible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server cannot trust the rendered form. A request may come from a stale browser tab, an older deployment, or a client calling the endpoint directly.&lt;/p&gt;

&lt;p&gt;The safer flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request schema
    |
    v
model catalog normalization
    |
    v
credit calculation
    |
    v
persist normalized task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker then receives normalized product input rather than raw form state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing Belongs in the Contract Too
&lt;/h2&gt;

&lt;p&gt;A flat "10 credits per generation" rule rarely survives a multi-model product.&lt;/p&gt;

&lt;p&gt;Image cost may depend on resolution. Video cost may depend on duration, quality, audio, and whether the user supplied video input. Provider pricing also changes over time.&lt;/p&gt;

&lt;p&gt;Catalog pricing can be expressed as versioned matching rules:&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;type&lt;/span&gt; &lt;span class="nx"&gt;CreditCostTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;credits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;effectiveAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;withAudio&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;withVideoInput&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resolver first chooses the latest price version effective at the task creation time. It then selects the most specific tier matching the normalized input.&lt;/p&gt;

&lt;p&gt;Two details matter here.&lt;/p&gt;

&lt;p&gt;First, calculate and persist the cost before the task enters the generation pipeline. A price update while the task is waiting should not change what the user already confirmed.&lt;/p&gt;

&lt;p&gt;Second, the UI estimate and server charge should use the same resolver. A price displayed on a button and a different price recorded in billing is a fast way to lose trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Catalog Semantics, Not Only Types
&lt;/h2&gt;

&lt;p&gt;TypeScript can validate the shape of an entry. It cannot prove that the entry makes sense.&lt;/p&gt;

&lt;p&gt;A correctly typed configuration can still contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a default value missing from its options&lt;/li&gt;
&lt;li&gt;a duplicate public slug&lt;/li&gt;
&lt;li&gt;an uncovered resolution-duration price combination&lt;/li&gt;
&lt;li&gt;a reference-enabled mode with a zero reference limit&lt;/li&gt;
&lt;li&gt;a provider route pointing at the wrong media kind&lt;/li&gt;
&lt;li&gt;a new price version that unintentionally changes historical calculations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catalog deserves focused tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every default is an allowed option
every public slug is unique
every supported mode normalizes successfully
every price combination resolves deterministically
every provider route matches the model kind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests also make reviews clearer. A model integration becomes a visible declaration of capabilities instead of a trail of special cases across unrelated files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Turn Everything Into JSON
&lt;/h2&gt;

&lt;p&gt;A catalog does not mean every difference must be forced into static configuration.&lt;/p&gt;

&lt;p&gt;Some models have real cross-field constraints. Some video prices depend on the measured duration of uploaded source assets. Those rules are easier to express as explicit pure functions called through the catalog's public API.&lt;/p&gt;

&lt;p&gt;A practical boundary is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;capabilities, options, defaults, and routes are data&lt;/li&gt;
&lt;li&gt;normalization, cross-field validation, and dynamic cost are pure functions&lt;/li&gt;
&lt;li&gt;request translation and response parsing stay in provider adapters&lt;/li&gt;
&lt;li&gt;retries and final settlement stay in the job workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to eliminate code. It is to give every difference one authoritative home.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff Is Predictability
&lt;/h2&gt;

&lt;p&gt;The complexity of a multi-model AI product does not come from the number of API clients. It comes from the number of capability combinations crossing the same product flow.&lt;/p&gt;

&lt;p&gt;Provider adapters hide integration differences. A model catalog gives the rest of the application one consistent interpretation of those differences.&lt;/p&gt;

&lt;p&gt;Once the catalog becomes a real product contract, adding a model stops meaning "add another exception everywhere." It becomes "declare a new set of capabilities inside a boundary we already understand."&lt;/p&gt;

&lt;p&gt;That predictability is more valuable than saving a few minutes on the next integration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <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>
