<?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>An AI Creation Product Is More Than a Model Picker</title>
      <dc:creator>Natalia</dc:creator>
      <pubDate>Sat, 12 Sep 2026 01:33:58 +0000</pubDate>
      <link>https://dev.to/natalia_c8ace1e6703e8f29f/an-ai-creation-product-is-more-than-a-model-picker-ek5</link>
      <guid>https://dev.to/natalia_c8ace1e6703e8f29f/an-ai-creation-product-is-more-than-a-model-picker-ek5</guid>
      <description>&lt;p&gt;The first version of an AI creation product is easy to imagine.&lt;/p&gt;

&lt;p&gt;Add a prompt box, connect a model API, wait for a result, and put a download button underneath it.&lt;/p&gt;

&lt;p&gt;That is enough for a demo. It is not enough for a product.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://sceneflare.com/" rel="noopener noreferrer"&gt;SceneFlare&lt;/a&gt;, an AI image and video creation platform, I kept running into the same gap. Users do not arrive with provider-shaped questions. They do not want to decide which API payload should contain a source image, how a callback should be retried, or why one model calls a setting &lt;code&gt;quality&lt;/code&gt; while another calls it &lt;code&gt;resolution&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They arrive with a creative goal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;turn a product photo into an ad concept&lt;/li&gt;
&lt;li&gt;animate a still image&lt;/li&gt;
&lt;li&gt;create several campaign directions&lt;/li&gt;
&lt;li&gt;remove a background before publishing&lt;/li&gt;
&lt;li&gt;improve an asset that is too small for its final placement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product has to translate that intent into a reliable generation workflow. The model call is only one step inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Job, Not the Model
&lt;/h2&gt;

&lt;p&gt;Model names matter to developers because they describe capabilities, cost, and provider behavior. They matter less to a user who needs a vertical product video before the end of the day.&lt;/p&gt;

&lt;p&gt;This changes how I think about navigation.&lt;/p&gt;

&lt;p&gt;A model-only directory asks the user to understand the implementation before using the product. A task-oriented surface starts with image creation, video creation, editing, or production tools, and then makes model choice visible where it helps.&lt;/p&gt;

&lt;p&gt;SceneFlare still has a &lt;a href="https://sceneflare.com/models" rel="noopener noreferrer"&gt;model directory&lt;/a&gt;. Hiding model identity would make comparison harder, especially when input modes and output controls differ. But the model is presented as part of a workflow, not as the entire workflow.&lt;/p&gt;

&lt;p&gt;The useful question is not only, "Which model is best?"&lt;/p&gt;

&lt;p&gt;It is, "Which configured workflow accepts the material I have and produces the asset I need?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Different Models Need One Product Language
&lt;/h2&gt;

&lt;p&gt;Image and video models rarely expose the same shape.&lt;/p&gt;

&lt;p&gt;One image model may support text-to-image and reference editing. Another may add transparent output or custom dimensions. A video model may accept a starting image, multiple references, source video, audio, or some combination of them. Duration and resolution options can change again between models.&lt;/p&gt;

&lt;p&gt;If every page speaks the provider's language directly, the product becomes a collection of unrelated forms.&lt;/p&gt;

&lt;p&gt;I prefer a product-owned vocabulary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mode describes the creative transformation&lt;/li&gt;
&lt;li&gt;source assets describe the material supplied by the user&lt;/li&gt;
&lt;li&gt;fields describe supported choices such as aspect ratio, duration, or quality&lt;/li&gt;
&lt;li&gt;outputs describe the assets returned to the user&lt;/li&gt;
&lt;li&gt;credits describe the price confirmed before submission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The internal model catalog maps that vocabulary to each model's real capabilities. The interface can stay consistent without pretending that all models are identical.&lt;/p&gt;

&lt;p&gt;This is an important distinction. Consistency does not mean flattening every difference. It means giving each difference one predictable place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Assets Are Part of the Request
&lt;/h2&gt;

&lt;p&gt;A prompt is simple: it is text that can be validated and sent with a request.&lt;/p&gt;

&lt;p&gt;Reference-led creation is less simple.&lt;/p&gt;

&lt;p&gt;The product has to know how many assets a mode accepts, which media types are valid, whether order matters, whether a video duration affects cost, and whether an uploaded image is a reference, a mask, or the primary input.&lt;/p&gt;

&lt;p&gt;That is why I do not think of upload as a generic attachment feature. Source assets are typed inputs to a creative operation.&lt;/p&gt;

&lt;p&gt;The form should prevent impossible combinations early. The server should validate the same rules again. A stale tab or a direct request should not be able to submit something the selected model cannot process.&lt;/p&gt;

&lt;p&gt;The browser is useful for feedback. The server is responsible for the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quote Before You Queue
&lt;/h2&gt;

&lt;p&gt;Pricing an AI media task is not always a flat lookup.&lt;/p&gt;

&lt;p&gt;An image request may depend on quality, output size, prompt length, reference count, or number of outputs. A video request may depend on duration, resolution, audio, and source media. Provider prices can also change while old tasks still need an understandable billing history.&lt;/p&gt;

&lt;p&gt;The safest point to resolve the product price is before the task enters the generation pipeline.&lt;/p&gt;

&lt;p&gt;The flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normalize input
    |
    v
calculate and show credit quote
    |
    v
confirm the request
    |
    v
freeze the quoted cost and queue the task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI estimate and the server charge must come from the same pricing rules, but the server still performs the authoritative calculation.&lt;/p&gt;

&lt;p&gt;A later tariff update should not silently change the cost of a task the user already confirmed. Persisting the pricing snapshot is not only an accounting decision. It is a trust decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generation Is an Asynchronous State Machine
&lt;/h2&gt;

&lt;p&gt;Image generation can be slow. Video generation makes this impossible to ignore.&lt;/p&gt;

&lt;p&gt;Once a provider accepts a task, several things can happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the result arrives through a callback&lt;/li&gt;
&lt;li&gt;polling observes completion first&lt;/li&gt;
&lt;li&gt;a timeout occurs after the provider accepted the request&lt;/li&gt;
&lt;li&gt;the provider returns multiple outputs&lt;/li&gt;
&lt;li&gt;an output transfer fails after generation succeeded&lt;/li&gt;
&lt;li&gt;moderation or final settlement needs to be retried&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A request handler should not pretend all of this is one long synchronous function.&lt;/p&gt;

&lt;p&gt;The user-visible task needs a durable state: pending, processing, completed, or failed. Provider attempts need their own history. Results need deterministic storage identities. Settlement needs to be safe to repeat.&lt;/p&gt;

&lt;p&gt;The uncomfortable case is an ambiguous submission. If the provider may have accepted the job but the local request timed out, immediately trying another provider can create two paid jobs. "Unknown" is a real state, even if it is not a pleasant one.&lt;/p&gt;

&lt;p&gt;Retries are easy to add. Correct retry boundaries are the actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result Should Become an Asset
&lt;/h2&gt;

&lt;p&gt;A generated URL is not the final product experience.&lt;/p&gt;

&lt;p&gt;Provider URLs may expire. One request may return several outputs. A user may want to compare a result, download it later, or reuse it as the source for another workflow.&lt;/p&gt;

&lt;p&gt;SceneFlare treats completed generations as assets in a library rather than temporary responses on a form. The &lt;a href="https://sceneflare.com/image" rel="noopener noreferrer"&gt;image&lt;/a&gt; and &lt;a href="https://sceneflare.com/video" rel="noopener noreferrer"&gt;video&lt;/a&gt; workflows may start differently, but their results should enter a familiar place with consistent status and download behavior.&lt;/p&gt;

&lt;p&gt;This also changes the backend boundary. Provider adapters should return normalized source results. Product storage should decide where final assets live. The adapter should not own the user's library structure.&lt;/p&gt;

&lt;p&gt;Provider integration and product ownership are related, but they are not the same responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety Has to Follow the Whole Workflow
&lt;/h2&gt;

&lt;p&gt;Prompt checks alone are not enough for an AI media product.&lt;/p&gt;

&lt;p&gt;A harmless prompt can include an unsafe reference image. A safe input can produce an output that violates policy. A video can contain a problematic moment that is easy to miss if only one frame is inspected.&lt;/p&gt;

&lt;p&gt;Safety therefore belongs at more than one boundary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;validate the prompt and request before spending generation credits&lt;/li&gt;
&lt;li&gt;inspect uploaded source assets where the workflow requires it&lt;/li&gt;
&lt;li&gt;inspect generated outputs before making them available&lt;/li&gt;
&lt;li&gt;keep the final business transition separate from the provider's success signal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A provider saying "completed" means the model finished. It does not necessarily mean the product is ready to publish the result.&lt;/p&gt;

&lt;p&gt;That difference is small in a diagram and important in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the Interface Boring
&lt;/h2&gt;

&lt;p&gt;The infrastructure behind a multi-model product can become complicated. The interface should not celebrate that complexity.&lt;/p&gt;

&lt;p&gt;The user needs to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what they can create&lt;/li&gt;
&lt;li&gt;which inputs are required&lt;/li&gt;
&lt;li&gt;what each control changes&lt;/li&gt;
&lt;li&gt;what the task will cost&lt;/li&gt;
&lt;li&gt;whether the task is waiting, running, complete, or failed&lt;/li&gt;
&lt;li&gt;where the result goes next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is an implementation detail until it helps answer one of those questions.&lt;/p&gt;

&lt;p&gt;That is the principle I keep returning to while working on SceneFlare. The platform can support prompt-based images, reference editing, text-to-video, image-to-video, background removal, upscaling, and new model families. The experience should still feel like one product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Is the Orchestration
&lt;/h2&gt;

&lt;p&gt;It is tempting to describe an AI creation platform by listing its models.&lt;/p&gt;

&lt;p&gt;The more useful description is the system around them.&lt;/p&gt;

&lt;p&gt;The catalog explains what each model can do. The form translates capabilities into valid choices. The quote makes cost explicit. The queue separates user requests from slow execution. The workflow handles uncertain provider states. Moderation protects the input and the result. The library turns a response into a reusable asset.&lt;/p&gt;

&lt;p&gt;The model creates the media.&lt;/p&gt;

&lt;p&gt;The product makes the creation process understandable and dependable.&lt;/p&gt;

&lt;p&gt;That is the part I am building with &lt;a href="https://sceneflare.com/" rel="noopener noreferrer"&gt;SceneFlare&lt;/a&gt;, and it is the part that becomes more important every time another model is added.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <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>
