<?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: neo xia</title>
    <description>The latest articles on DEV Community by neo xia (@neo_xia_3f4c019330af5fb9d).</description>
    <link>https://dev.to/neo_xia_3f4c019330af5fb9d</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%2F3961904%2F1a660dc3-e1fb-4caf-b9fe-c28d09fd5a72.png</url>
      <title>DEV Community: neo xia</title>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neo_xia_3f4c019330af5fb9d"/>
    <language>en</language>
    <item>
      <title>Next.js 16 on Cloudflare Workers: what broke and what didn't</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Wed, 22 Jul 2026 03:23:39 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/nextjs-16-on-cloudflare-workers-what-broke-and-what-didnt-5bka</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/nextjs-16-on-cloudflare-workers-what-broke-and-what-didnt-5bka</guid>
      <description>&lt;p&gt;I shipped a Next.js 16 app on Cloudflare Workers via OpenNext. Not a demo. A real product with streaming chat, server components, D1 at the edge, and anonymous user sessions.&lt;/p&gt;

&lt;p&gt;Here is what broke, what barely worked, and what turned out to be surprisingly fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16.2 (App Router)&lt;/li&gt;
&lt;li&gt;@opennextjs/cloudflare 1.19&lt;/li&gt;
&lt;li&gt;D1 for SQLite at the edge&lt;/li&gt;
&lt;li&gt;Streaming chat via the AI binding (DeepSeek-V3 through a Workers proxy)&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;Tailwind CSS 4&lt;/li&gt;
&lt;li&gt;No auth wall, no OAuth, no database on the origin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site runs a few thousand sessions a week across ~30 persona pages, blog posts, guides, and learning content. Most pages are statically generated. The chat interaction is server-rendered components with streaming responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked surprisingly well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Static generation and ISR
&lt;/h3&gt;

&lt;p&gt;Pages, blogs, guides, persona pages — everything that does not need user-specific rendering — runs as static HTML at deploy time.&lt;/p&gt;

&lt;p&gt;Next.js 16 with &lt;code&gt;generateStaticParams&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; caching worked without modification. OpenNext handles the Cloudflare output format. The build step produces something Workers can serve. Revalidations are limited to Workers' cache API, but since most content changes at deploy time, I never hit that limit in production.&lt;/p&gt;

&lt;p&gt;The one caveat: &lt;code&gt;revalidateTag()&lt;/code&gt; does not work the same way in a Workers runtime. Tags are Node.js memory constructs, and Workers are stateless. If you depend on tag-based revalidation for content updates, you need to either trigger deploys or accept stale-while-revalidate behavior from the CDN.&lt;/p&gt;

&lt;h3&gt;
  
  
  D1 at the edge
&lt;/h3&gt;

&lt;p&gt;D1 was the least surprising part of the stack. SQL queries from Next.js route handlers feel like calling a regular database. Sessions store in D1, messages store in D1, and the latency is low enough that restoring a full chat thread from 30 messages takes under 200ms cold.&lt;/p&gt;

&lt;p&gt;The only sharp edge: D1 connections count against your Worker's concurrent request limit in development. With Next.js making its own fetch calls for compilation, I hit the D1 connection ceiling faster than expected. The fix was moving &lt;code&gt;wrangler dev&lt;/code&gt; to use &lt;code&gt;--experimental-json-config&lt;/code&gt; early, but the dev experience for D1 + Next.js hot reload is still rougher than it should be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming chat responses
&lt;/h3&gt;

&lt;p&gt;The app streams model responses token by token. In dev, this was unreliable — the stream would drop mid-response on roughly 1 in 15 requests. I spent two days tracing it before realizing it was a local &lt;code&gt;wrangler dev&lt;/code&gt; issue with HTTP chunked transfer encoding, not a production problem.&lt;/p&gt;

&lt;p&gt;In production, streaming over Workers works. The AI binding handles the model request inside the Worker, and the response streams back through the Next.js route handler to the client without a hitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymous user sessions
&lt;/h3&gt;

&lt;p&gt;The product ships without required sign-in. Users get a UUID from &lt;code&gt;crypto.randomUUID()&lt;/code&gt; stored in localStorage, sent as &lt;code&gt;X-User-Id&lt;/code&gt; on every chat request. On the server, D1 writes sessions against that ID.&lt;/p&gt;

&lt;p&gt;Workers not having durable session state actually helps here. The identity arrives in headers. No sticky sessions. No session table. The Worker pulls the user ID, checks D1 for existing sessions or quota limits, and proceeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Middleware and edge runtime mismatch
&lt;/h3&gt;

&lt;p&gt;Next.js 16 middleware runs on every request. On Cloudflare Workers, middleware executes in the edge runtime, not the Node.js runtime. This means any middleware that imports &lt;code&gt;crypto&lt;/code&gt; or uses APIs outside the Workers subset will fail at request time, not at build time.&lt;/p&gt;

&lt;p&gt;I had a middleware that checked rate limits using a counter in D1. Simple enough. But the D1 binding is not available in the middleware context through OpenNext the way it is in route handlers. I had to move rate limiting to a Cloudflare WAF rule instead of doing it in Next.js middleware. Not a dealbreaker, but the middleware → Workers gap is larger than the documentation suggests.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;next/image&lt;/code&gt; and Workers
&lt;/h3&gt;

&lt;p&gt;Static images work fine. Dynamic image optimization through &lt;code&gt;next/image&lt;/code&gt; does not, because Workers lack the image processing libraries that Node.js uses.&lt;/p&gt;

&lt;p&gt;The fix: I pre-processed all images at build time and served them as static assets. No runtime optimization needed. For an app with fewer than 50 unique images, this was trivial. For an app with user-uploaded content, it would be a hard problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables in client components
&lt;/h3&gt;

&lt;p&gt;Environment variables prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; are baked into the client bundle at build time. That works fine. But runtime environment variables accessed through &lt;code&gt;process.env&lt;/code&gt; in server components behave differently in Workers.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers use a &lt;code&gt;env&lt;/code&gt; binding, not &lt;code&gt;process.env&lt;/code&gt;. OpenNext bridges this, but the bridge is not seamless. Variables I set in &lt;code&gt;wrangler.toml&lt;/code&gt; were available in the Worker context but not through the &lt;code&gt;process.env&lt;/code&gt; API that Next.js server components expect at runtime. The workaround was importing the Cloudflare bindings through the &lt;code&gt;@opennextjs/cloudflare&lt;/code&gt; types and passing them explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first-deploy cold start
&lt;/h3&gt;

&lt;p&gt;First deploy after building with OpenNext takes roughly 45-60 seconds for the first Worker request. After the initial cold start, response times drop to normal.&lt;/p&gt;

&lt;p&gt;This is a known Workers characteristic compounded by Next.js route chunking. The more routes your app has, the more chunks the Worker needs to load on the first request. My app has about 30 routes. A smaller app with 5 routes would cold-start faster.&lt;/p&gt;

&lt;p&gt;Subsequent deploys are faster because Cloudflare caches the compilation output. But the first deploy of the day always has a cold start window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming + D1 in the same route handler
&lt;/h3&gt;

&lt;p&gt;This one was subtle. A route handler that reads from D1 and then streams a response — for example, loading session history and then starting the chat stream — sometimes lost the D1 response before the stream completed.&lt;/p&gt;

&lt;p&gt;The issue: Workers terminate the request context after the response is sent. If you read from D1 inside the same handler that starts a streaming response, the D1 bindings can close before the stream finishes if the stream outlasts the initial response resolution.&lt;/p&gt;

&lt;p&gt;The fix: resolve all D1 reads before starting the stream. Load session data early, store it in a closure, then begin streaming.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: D1 read interleaved with streaming&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM sessions WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;streamToReadableStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: resolve D1 reads first, then stream&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM sessions WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messagesWithHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;newMessages&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messagesWithHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;streamToReadableStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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;Straightforward fix once I recognized the pattern. Cost me two evenings of debugging before I found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am still watching
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ISR revalidation at scale.&lt;/strong&gt; I have not reached the threshold where Workers cache API limits matter. If the app grows to hundreds of content pages with frequent revalidations, I may need to revisit the caching strategy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;D1 row limits.&lt;/strong&gt; A few thousand sessions with 10-15 messages each is well under D1's limits. If the app crosses 100k+ sessions, I will need either a message archiving strategy or a move to R2 for stored messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenNext churn.&lt;/strong&gt; The OpenNext → Cloudflare pipeline changes version to version. Two minor bumps have already required &lt;code&gt;wrangler.toml&lt;/code&gt; changes. The stack is stable enough for production but not mature enough to set and forget.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;p&gt;Next.js 16 on Cloudflare Workers works for a real production app. The static generation path is smooth. D1 is solid. Streaming is fine after you learn the D1-resolution-before-stream rule.&lt;/p&gt;

&lt;p&gt;The rough edges are in the gaps between ecosystems: middleware runtimes, environment variable access patterns, and image optimization. Each gap has a workaround, but the workarounds are not always documented.&lt;/p&gt;

&lt;p&gt;The OpenNext team has done the heavy lift of bridging Next.js to Workers. The remaining friction is mostly about accepting that you are on a serverless platform with different constraints than Vercel. Once you internalize those constraints — resolve D1 first, pre-process images, avoid Node.js APIs in middleware — the stack holds.&lt;/p&gt;

&lt;p&gt;For a production app with streaming chat, anonymous sessions, and static content: &lt;a href="https://cosskill.com?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=week8-nextjs-workers" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;. The stack file is pinned on the repo if you want the full picture.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>nextjs</category>
      <category>cloudflarechallenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Chose DeepSeek Over GPT-4 for a Free AI Conversation App</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 06:43:39 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/why-i-chose-deepseek-over-gpt-4-for-a-free-ai-conversation-app-3gci</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/why-i-chose-deepseek-over-gpt-4-for-a-free-ai-conversation-app-3gci</guid>
      <description>&lt;p&gt;I did not choose DeepSeek because I think GPT-4 is bad. I chose it because I was building a free app, and free apps teach you what actually matters pretty fast.&lt;/p&gt;

&lt;p&gt;The question was simple: how do I keep sessions cheap enough that people can practice a lot without me lighting money on fire?&lt;/p&gt;

&lt;p&gt;The answer pushed me toward DeepSeek-V3 (and later R1 for specific tasks).&lt;/p&gt;

&lt;h2&gt;
  
  
  The real constraint was volume
&lt;/h2&gt;

&lt;p&gt;The app is a conversation practice tool. People come in to rehearse hard talks, not to admire the model.&lt;/p&gt;

&lt;p&gt;A single practice session runs 8-15 turns. Each turn is roughly 300-600 tokens in, 100-300 out. Multiply that by five sessions a week per active user and the costs start compounding.&lt;/p&gt;

&lt;p&gt;Here is what the math looked like when I was choosing (mid-2026 pricing):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input cost (per 1M tokens)&lt;/th&gt;
&lt;th&gt;Output cost (per 1M tokens)&lt;/th&gt;
&lt;th&gt;Cost per 10-turn session (est.)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;~$0.04-0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4 Turbo&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;$30.00&lt;/td&gt;
&lt;td&gt;~$0.12-0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-V3&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;td&gt;$1.10&lt;/td&gt;
&lt;td&gt;~$0.004-0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1&lt;/td&gt;
&lt;td&gt;$0.55&lt;/td&gt;
&lt;td&gt;$2.19&lt;/td&gt;
&lt;td&gt;~$0.008-0.012&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale, the difference between $0.005 and $0.05 per session is the difference between running a free product and needing a paywall after three conversations. I wanted people to come back daily without hitting a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DeepSeek handled well
&lt;/h2&gt;

&lt;p&gt;It stayed in character for 10-15 turns. It pushed back when the user got vague. It followed persona heuristics (numbered if/then rules in the system prompt) about as reliably as GPT-4o did for our use case.&lt;/p&gt;

&lt;p&gt;For salary negotiation rehearsal, the model needs to say "that's not in the budget" and hold that position for three more turns while the user tries different approaches. DeepSeek-V3 did this. Not perfectly, but reliably enough that sessions felt real.&lt;/p&gt;

&lt;p&gt;It also made the app easier to run as a free product. People can try, fail, reset, and try again without me worrying about per-session cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where GPT-4 was still better
&lt;/h2&gt;

&lt;p&gt;GPT-4 (and 4o) is smoother with nuanced emotional wording. When a conversation gets subtle, loaded with subtext, or requires picking up on implied meaning, GPT-4 catches more.&lt;/p&gt;

&lt;p&gt;For the breakup text persona, GPT-4o noticed when a user's "kind" message was actually passive-aggressive. DeepSeek missed that about 20% more often in my informal testing across ~100 sessions.&lt;/p&gt;

&lt;p&gt;But polish was not the main bottleneck for this product. The main bottleneck was getting people enough reps to build actual comfort with discomfort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff I actually cared about
&lt;/h2&gt;

&lt;p&gt;Do I want one beautiful session, or ten useful ones?&lt;/p&gt;

&lt;p&gt;For this app, ten useful ones. Every time.&lt;/p&gt;

&lt;p&gt;So I took the cheaper model, put the engineering effort into the prompt architecture (persona seed, heuristics, mode wrapper, boundaries), and accepted that 85-90% quality at 10x the volume was a better product than 95% quality at 1x.&lt;/p&gt;

&lt;p&gt;The model matters. The scaffolding around it matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed to make DeepSeek work
&lt;/h2&gt;

&lt;p&gt;A few things made the choice viable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tighter system prompts. DeepSeek drifts more with long, loose instructions. Shorter seed, more numbered rules.&lt;/li&gt;
&lt;li&gt;Lower temperature (0.55 for roleplay, 0.2 for scoring). Kept persona variation without character breaks.&lt;/li&gt;
&lt;li&gt;Max reply length cap in the mode wrapper. DeepSeek's default is wordier than GPT-4o, so I had to constrain it explicitly.&lt;/li&gt;
&lt;li&gt;Built retries into the flow. A bad response does not kill the session; the user gets a fresh turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is underrated for any practice app. The experience should not feel fragile.&lt;/p&gt;

&lt;h2&gt;
  
  
  My actual takeaway
&lt;/h2&gt;

&lt;p&gt;If you are building a free AI app, the best model is not always the smartest one. It is the one that lets people come back tomorrow.&lt;/p&gt;

&lt;p&gt;Not bragging rights. Not benchmark charts. Whether the app stays affordable enough to be used like a tool instead of a demo.&lt;/p&gt;

&lt;p&gt;For cosskill, DeepSeek made more sense. It let me build something people use five times a week instead of once and forget. Which is usually the whole game for a practice product anyway.&lt;/p&gt;

&lt;p&gt;If you want to see the product, it is at &lt;a href="https://cosskill.com?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=week5-deepseek" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built an AI conversation simulator because I kept chickening out of real talks</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Mon, 01 Jun 2026 03:43:42 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-conversation-simulator-because-i-kept-chickening-out-of-real-talks-2jmc</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-conversation-simulator-because-i-kept-chickening-out-of-real-talks-2jmc</guid>
      <description>&lt;p&gt;Last year I needed to ask for a raise. I knew my number, I'd read the guides, I had bullet points in my notes app. Then my manager said "let's chat about your goals for next quarter" and I said "sounds great, looking forward to it" and hung up. Never brought up money.&lt;/p&gt;

&lt;p&gt;Same thing kept happening elsewhere. Coworker taking credit for my work, I said nothing. Relationship that should've ended months earlier, I kept postponing. I always knew what to say. I just couldn't say it with someone actually looking at me.&lt;/p&gt;

&lt;p&gt;So I started building a thing to practice on. That thing became &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;You pick a persona, tell it the situation in a sentence, and start talking. The persona doesn't help you. It holds position and pushes back. You practice not folding.&lt;/p&gt;

&lt;p&gt;Think of it as a flight simulator for hard conversations. You rehearse until your opener comes out steady, then go do the real thing. 20 personas across five categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operators (Musk, Jobs): first-principles thinking, harsh product feedback&lt;/li&gt;
&lt;li&gt;Strategists (Trump, Buffett): treat everything as a deal or a bet&lt;/li&gt;
&lt;li&gt;Relationship (Ex, Coworker): breakups, workplace friction, family money&lt;/li&gt;
&lt;li&gt;Philosophy (Socrates, Aurelius, Confucius, Sun Tzu, four more): each tradition frames problems differently&lt;/li&gt;
&lt;li&gt;Psychology (Rogers, Rosenberg, Ellis, Frankl, Kahneman, Jung): therapeutic frameworks on real situations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't celebrity impressions. The Buffett persona won't hype your startup idea. It'll ask "what's the downside?" and keep asking until you have something concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Next.js 16 on Cloudflare Workers. DeepSeek for inference. Cloudflare D1 (SQLite at edge) for the bits that need to persist. No user accounts, chat history lives in localStorage.&lt;/p&gt;

&lt;p&gt;Monthly cost stays low enough that the free tier (10 messages/day) doesn't worry me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made these choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek instead of GPT-4/Claude.&lt;/strong&gt; Each conversation is 10-30 messages. At GPT-4 pricing a free product bleeds money. DeepSeek gives maybe 90% of the quality for a fraction of the cost on this specific task, which is maintaining persona consistency across a back-and-forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No accounts.&lt;/strong&gt; Every signup form is friction between "I need to practice this talk" and actually practicing it. If I add a login wall, some percentage of people close the tab and go back to rehearsing in the shower. I'd rather have them practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Workers instead of Vercel.&lt;/strong&gt; D1 is genuinely good for this. One database at edge, no connection pooling, no separate DB service. DX is slightly worse for Next.js specifically but the deploy simplicity makes up for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personas that resist instead of a chatbot that helps.&lt;/strong&gt; A generic AI assistant will agree with you. That's the problem. You don't need agreement. You need someone saying "that's not in the budget" while you practice not conceding immediately. Each persona has hardcoded positions and pushback patterns. They're useful by being difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned building this
&lt;/h2&gt;

&lt;p&gt;Prompting a persona to stay in character is a solved problem. The hard part was figuring out what "practice" means for conversations.&lt;/p&gt;

&lt;p&gt;Scripts don't work because they sound robotic and shatter on contact with a real human. Free-form chatting doesn't work because there's no improvement loop. What actually works: have one sentence you want to say, say it to something that pushes back, adjust based on what happens, try again. The first two minutes of any hard conversation set the rest. So you practice those two minutes.&lt;/p&gt;

&lt;p&gt;After a few runs, people say the real conversation feels less scary. Not because they memorized a script. Because they already heard the worst response and survived. "That's not in the budget" doesn't feel like a gut punch when you've heard it four times from an AI and practiced not caving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;Early. Traffic is growing organically. The pages that get the most hits are salary negotiation practice and breakup text help. Draw your own conclusions about what people actually struggle with.&lt;/p&gt;

&lt;p&gt;Free tier handles most users. Pro ($9.90/month annual) is for people who want unlimited messages and custom personas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;. No signup, just pick a persona and start. If you have a salary conversation coming up, try Buffett. Need to set a boundary with a coworker, try the Coworker persona. Want someone to tear your startup pitch apart, try Jobs.&lt;/p&gt;

&lt;p&gt;Genuinely curious: what conversations do devs specifically need to practice that I haven't thought of? And what persona doesn't exist yet but should?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>first time to here</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Mon, 01 Jun 2026 03:34:12 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/first-time-to-here-4if8</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/first-time-to-here-4if8</guid>
      <description></description>
    </item>
  </channel>
</rss>
