<?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: Paweł Gałązka</title>
    <description>The latest articles on DEV Community by Paweł Gałązka (@pawelgalazka).</description>
    <link>https://dev.to/pawelgalazka</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%2F3895862%2F878cb674-b3e3-40ba-bef5-9514b8a786b2.jpeg</url>
      <title>DEV Community: Paweł Gałązka</title>
      <link>https://dev.to/pawelgalazka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawelgalazka"/>
    <language>en</language>
    <item>
      <title>Embracing TanStack Start for full-stack development on Cloudflare - PageZERO v2</title>
      <dc:creator>Paweł Gałązka</dc:creator>
      <pubDate>Tue, 14 Jul 2026 10:22:13 +0000</pubDate>
      <link>https://dev.to/pawelgalazka/embracing-tanstack-start-for-full-stack-development-on-cloudflare-pagezero-v2-51ge</link>
      <guid>https://dev.to/pawelgalazka/embracing-tanstack-start-for-full-stack-development-on-cloudflare-pagezero-v2-51ge</guid>
      <description>&lt;p&gt;If you have not run into &lt;a href="https://pagezero.dev" rel="noopener noreferrer"&gt;PageZERO&lt;/a&gt; before, it is an open source, Cloudflare-native web app starter. You get auth, payments, permissions, email, blog, newsletter, a database layer, a tested CI/CD pipeline, and more, running on Cloudflare Workers and Cloudflare D1 database.&lt;/p&gt;

&lt;p&gt;It requires &lt;a href="https://bun.com" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; to be installed. Once you have it, start with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx pagezero@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the foundation. Today I want to talk about what changed in &lt;strong&gt;v2&lt;/strong&gt;, the largest release since the project began, and &lt;strong&gt;TanStack Start&lt;/strong&gt; is at the center of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From React Router v7 to TanStack Start
&lt;/h2&gt;

&lt;p&gt;PageZERO v1 was built on React Router v7. Unfortunately, the strategic picture shifted. The core React Router team is focused on Remix 3, which is no longer React-based. Community momentum is fading, and betting the web app foundation on a framework moving away from React is a risk not worth taking. That's why PageZERO v2 swaps React Router v7 for TanStack Start.&lt;/p&gt;

&lt;p&gt;The main difference in practice is that React Router's loaders and actions, which doubled &lt;br&gt;
as ad hoc API endpoints, are replaced by TanStack Start's &lt;strong&gt;server functions&lt;/strong&gt;. &lt;br&gt;
React Router &lt;strong&gt;loaders&lt;/strong&gt; fetch the data a route needs before it renders, while &lt;strong&gt;actions&lt;/strong&gt; &lt;br&gt;
handle mutations, like form submissions that create or update data.&lt;/p&gt;

&lt;p&gt;React Router &lt;strong&gt;loader&lt;/strong&gt;:&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;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;loader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LoaderArgs&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&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;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsTable&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProjectsPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;loaderData&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProjectsList&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loaderData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&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;TanStack Start &lt;em&gt;equivalent&lt;/em&gt;:&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;createServerFn&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&lt;/span&gt;&lt;span class="dl"&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;createFileRoute&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-router&lt;/span&gt;&lt;span class="dl"&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;getMainDb&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;@/db/main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProjects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerFn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMainDb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&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;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsTable&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getProjects&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProjectsPage&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;React Router &lt;strong&gt;action&lt;/strong&gt;:&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;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;action&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="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionArgs&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbContext&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;formData&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsTable&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TanStack Start &lt;em&gt;equivalent&lt;/em&gt;:&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;createServerFn&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&lt;/span&gt;&lt;span class="dl"&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;getMainDb&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;@/db/main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createProject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerFn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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;validator&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMainDb&lt;/span&gt;&lt;span class="p"&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsTable&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;A few concrete wins fall out of that shift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simpler client-server communication.&lt;/strong&gt; Server functions handle the request and response behind the scenes, so calling server code from the client feels like calling a regular function, no route or URL to define by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end type safety.&lt;/strong&gt; Return types and validated input flow straight from server function to caller, no &lt;code&gt;Route.LoaderArgs&lt;/code&gt; or &lt;code&gt;Route.ActionArgs&lt;/code&gt; generics to pass around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One RPC layer for everything.&lt;/strong&gt; &lt;code&gt;rpc/&lt;/code&gt; is now the single place callable server logic lives, instead of splitting it across page routes, resource routes, and ad hoc API endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable by default.&lt;/strong&gt; One server function can be called from multiple loaders, a &lt;code&gt;beforeLoad&lt;/code&gt; guard, a form submission, or a client event handler, no more duplicating logic across a page route and a resource route just to reach it from two places.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TanStack Start and Cloudflare
&lt;/h2&gt;

&lt;p&gt;TanStack Start runs on Cloudflare Workers without any adapter gymnastics (unlike Next.js). &lt;code&gt;wrangler.json&lt;/code&gt; simply points its &lt;code&gt;main&lt;/code&gt; entry at the framework's own server bundle:&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;"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="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;Wrangler builds that entry, hands it to a Worker, and server-side rendering just works: every &lt;br&gt;
route renders on the edge, close to the user.&lt;/p&gt;

&lt;p&gt;Like React Router before it, TanStack Start itself is just a Vite plugin, &lt;code&gt;tanstackStart()&lt;/code&gt;, dropped into &lt;code&gt;vite.config.ts&lt;/code&gt; alongside the Cloudflare plugin:&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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&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="p"&gt;...&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That plugin also handles static prerendering: pages you mark for it are built to static HTML at deploy time and served straight from Cloudflare's CDN via the &lt;code&gt;assets&lt;/code&gt; binding, bypassing the Worker entirely. Everything else keeps rendering on demand through the Worker. Same codebase, same routes, and the right pages are served from the CDN without a separate static site to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simpler deployment to Cloudflare
&lt;/h2&gt;

&lt;p&gt;v1 required manual work before a first deploy: create each D1 database with &lt;code&gt;wrangler d1 create&lt;/code&gt;, paste the ID into &lt;code&gt;wrangler.json&lt;/code&gt;, then run a bespoke &lt;code&gt;db:setup&lt;/code&gt; chain. v2 drops that ceremony: Wrangler provisions D1 databases automatically on first deploy, and migrations run through &lt;code&gt;wrangler d1 migrations apply&lt;/code&gt; instead of &lt;code&gt;drizzle-kit migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A first deploy now looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log in to Cloudflare&lt;/strong&gt; with &lt;code&gt;bunx wrangler login&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset &lt;code&gt;wrangler.json&lt;/code&gt; for your project&lt;/strong&gt; with &lt;code&gt;bun setup:wrangler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; with &lt;code&gt;bun deploy:preview&lt;/code&gt; and &lt;code&gt;bun deploy:production&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply migrations&lt;/strong&gt; with &lt;code&gt;bun db:migrate --env preview --remote&lt;/code&gt; and &lt;code&gt;bun db:migrate --env production --remote&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Push-to-deploy through GitHub Actions still works the same way: every merge to &lt;code&gt;main&lt;/code&gt; ships to production and every PR gets a preview environment, with migrations applied automatically per environment via the official &lt;code&gt;cloudflare/wrangler-action&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A built-in blog
&lt;/h2&gt;

&lt;p&gt;v2 ships a file-based blog out of the box. Posts live as &lt;code&gt;.mdx&lt;/code&gt; files in &lt;code&gt;apps/blog/content/&lt;/code&gt;. Drop one in and it shows up on the blog index automatically, served at &lt;code&gt;/blog/&amp;lt;slug&amp;gt;&lt;/code&gt; with the slug derived from the filename. No route registration step. A minimal post 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="nx"&gt;cover&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;../assets/hello-cover.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My first post.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imgSrc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&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;Jane Doe&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="nx"&gt;Regular&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Markdown&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;works&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alongside&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt; &lt;span class="nx"&gt;you&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each post exports a &lt;code&gt;frontmatter&lt;/code&gt; object (title, description, date, cover image, author) that is validated with Zod at load time, so a missing field fails loudly at build instead of quietly producing a broken page. Posts get SEO meta tags from that frontmatter automatically, code blocks are syntax-highlighted, and the index is sorted newest-first. The post you are reading right now is, fittingly, just such an MDX file.&lt;/p&gt;

&lt;h2&gt;
  
  
  MDX static content
&lt;/h2&gt;

&lt;p&gt;The blog is really one instance of a more general capability: MDX content compiled at build time into React components. Frontmatter is written as a JS export rather than YAML, which means a content file can import local image assets processed by Vite and reference live config values directly.&lt;/p&gt;

&lt;p&gt;The same &lt;code&gt;getMdxModuleBySlug&lt;/code&gt; helper and &lt;code&gt;MDXProvider&lt;/code&gt; that power the blog route work for any content area. Here is a docs route built the same way:&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;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notFound&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-router&lt;/span&gt;&lt;span class="dl"&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;getMdxModuleBySlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MDXProvider&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;@/mdx&lt;/span&gt;&lt;span class="dl"&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;docModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docFrontmatterSchema&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;../content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/docs/$slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DocPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DocPage&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useParams&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;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMdxModuleBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docFrontmatterSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&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;DocComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;DocComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MDXProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DocComponent&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MDXProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;v2 uses this for more than blogging. Legal pages moved to MDX files, and a shared &lt;code&gt;ProseArticle&lt;/code&gt; layout handles any read-focused page. Adding a new content area follows the same shape as the docs route above: a content folder with an &lt;code&gt;index.ts&lt;/code&gt; globbing its &lt;code&gt;.mdx&lt;/code&gt; files, an index and detail route, both registered in &lt;code&gt;apps/routes.ts&lt;/code&gt;. Static prerendering and sitemap generation are scaffolded in &lt;code&gt;vite.config.ts&lt;/code&gt; and ready to switch on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A newsletter with double opt-in
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;newsletter&lt;/code&gt; app adds a complete subscription flow powered by Resend. A subscriber enters their email in the ready-made &lt;code&gt;NewsletterSignup&lt;/code&gt; component, receives a confirmation email, and is only added to your Resend audience after they click through. A proper double opt-in that keeps your list clean and compliant.&lt;/p&gt;

&lt;p&gt;Drop the form into any route. When Cloudflare Turnstile is configured, load the public key in the route loader and pass it through, otherwise omit the prop and bot protection stays off:&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;NewsletterSignup&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;@/newsletter/components/newsletter-signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NewsletterSignup&lt;/span&gt;
  &lt;span class="nx"&gt;cloudflareTurnstilePublicKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cloudflareTurnstilePublicKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details are handled for you: confirmation links are signed and expire after 30 minutes, Cloudflare Turnstile bot protection is optional, and the contact lands in a configured audience segment on confirmation. The signup form is already embedded on the home page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type-safe forms
&lt;/h2&gt;

&lt;p&gt;Building forms by hand is tedious and easy to get subtly wrong, so v2 introduces a small &lt;code&gt;@/form&lt;/code&gt; package that ties together TanStack Start server functions, TanStack Query mutations, and Zod validation around &lt;strong&gt;one&lt;/strong&gt; shared Zod schema. Rather than pulling in a full form-state library like TanStack Form, it stays deliberately thin: native &lt;code&gt;FormData&lt;/code&gt;, a single hook, and a schema you already have.&lt;/p&gt;

&lt;p&gt;Here is the client side of it, a form wired up with the &lt;code&gt;useFormAction&lt;/code&gt; hook against a server function that validates incoming data with that same schema:&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;useFormAction&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;@/form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContactForm&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFormAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;noValidate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sending...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;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;&lt;code&gt;useFormAction&lt;/code&gt; wraps the server function in a TanStack Query mutation and hands back an &lt;code&gt;onSubmit&lt;/code&gt; plus typed &lt;code&gt;fields&lt;/code&gt; with per-field validation errors, so you never manage individual input state by hand. The schema is the single source of truth, so field names, types, and error messages stay in sync. The newsletter signup is built on exactly this primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready for horizontally scaling D1
&lt;/h2&gt;

&lt;p&gt;Cloudflare D1 database is designed for horizontal scaling: many small, independent SQLite databases rather than one giant one. v1 didn't leave room for that, everything lived behind a single &lt;code&gt;DB&lt;/code&gt; binding at &lt;code&gt;packages/db/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;v2 renames it to &lt;code&gt;DB_MAIN&lt;/code&gt; and moves it into its own &lt;code&gt;packages/db/main/&lt;/code&gt; folder with its own schema and migrations. That &lt;code&gt;DB_&amp;lt;NAME&amp;gt;&lt;/code&gt; / &lt;code&gt;packages/db/&amp;lt;name&amp;gt;/&lt;/code&gt; convention is now the pattern to follow: adding a second database, e.g. &lt;code&gt;DB_SECONDARY&lt;/code&gt;, means registering the binding in &lt;code&gt;wrangler.json&lt;/code&gt;, adding a matching &lt;code&gt;packages/db/secondary/&lt;/code&gt; folder with a &lt;code&gt;getSecondaryDb()&lt;/code&gt;, and reusing the existing scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DB_BINDING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DB_SECONDARY bun run db:generate
&lt;span class="nv"&gt;DB_BINDING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DB_SECONDARY bun run db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The starter still ships with a single &lt;code&gt;DB_MAIN&lt;/code&gt; by default, but the seams for splitting off a second one (per tenant, per domain, per region) are already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If this is your first time hearing about PageZERO, everything above (TanStack Start, the blog, the newsletter, type-safe forms, MDX content, the simplified deployment flow, and the groundwork for multiple databases) ships out of the box the moment you generate a project. One command and you can start building your app on solid foundations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx pagezero@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the full picture, check &lt;a href="https://pagezero.dev" rel="noopener noreferrer"&gt;pagezero.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>react</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Meet PageZERO: an open source SaaS starter kit on Cloudflare for the AI era</title>
      <dc:creator>Paweł Gałązka</dc:creator>
      <pubDate>Thu, 04 Jun 2026 10:40:01 +0000</pubDate>
      <link>https://dev.to/pawelgalazka/meet-pagezero-an-open-source-saas-starter-kit-on-cloudflare-for-the-ai-era-1la1</link>
      <guid>https://dev.to/pawelgalazka/meet-pagezero-an-open-source-saas-starter-kit-on-cloudflare-for-the-ai-era-1la1</guid>
      <description>&lt;p&gt;You have an idea for a SaaS. You open Cursor or Claude Code, and the familiar ritual begins. Auth. A database. Payments. Transactional email. Permissions. The agent generates it. You spend the weekend reviewing and patching, you burn $100 to $300 in tokens on the same foundational plumbing every serious project requires, and half of what comes back has subtle security holes or broken logic that only surfaces once a real user signs up. Welcome to AI slop.&lt;/p&gt;

&lt;p&gt;Then comes the second surprise. The project grows, the free tier ends, and the infrastructure bill arrives. Egress fees on every byte that leaves the network. A managed database that costs money even while it sits idle overnight. Pricing that grows faster than expected.&lt;/p&gt;

&lt;p&gt;PageZERO exists to remove both of those problems. It is an open source, full stack codebase that wires together everything a serious SaaS needs, with conventions an AI agent can actually follow, and it runs entirely on Cloudflare, so your infrastructure starts free and scales for pennies. This is the first real post on this blog, so let me introduce what it is and why it is built the way it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PageZERO is: a SaaS starter kit you own
&lt;/h2&gt;

&lt;p&gt;PageZERO is a complete starter codebase you own outright. Run one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx pagezero@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a complete TypeScript project with authentication, payments, role based permissions, email, a database layer, UI components, testing, and a full CI/CD pipeline already connected and tested. You write product code on day one instead of glue code.&lt;/p&gt;

&lt;p&gt;It is important to be precise about what PageZERO is not. It is not a framework and not a hosted service. There is no runtime dependency on PageZERO, no contract to honor, no upgrade treadmill. After the init command you have a normal repository. Keep what you need, delete what you do not, replace anything you disagree with. Every line is yours under the MIT license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare, and how it works
&lt;/h2&gt;

&lt;p&gt;The most distinctive choice in PageZERO is that it is Cloudflare native from top to bottom. Most starters assume Vercel for hosting and a separate provider for the database. PageZERO instead uses one integrated platform, and that decision drives both the architecture and the economics.&lt;/p&gt;

&lt;p&gt;Here are the parts of Cloudflare a PageZERO app uses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Cloudflare service&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;Runs server side rendering and API routes in 300+ cities, close to the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;A serverless SQLite database that lives at the edge next to your Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static files&lt;/td&gt;
&lt;td&gt;Assets&lt;/td&gt;
&lt;td&gt;Serves JS, CSS, and images from the global CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot protection&lt;/td&gt;
&lt;td&gt;Turnstile&lt;/td&gt;
&lt;td&gt;Blocks form spam with an invisible challenge, no annoying CAPTCHA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because compute and data sit in the same edge network, requests do not hop across regions to reach a distant database. Code runs in the data center nearest the visitor, and Workers have no cold starts, so there is no slow first request after a quiet period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free tier
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;🟠 Cloudflare&lt;/th&gt;
&lt;th&gt;▲ Vercel&lt;/th&gt;
&lt;th&gt;☁️ AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;3M/mo&lt;/td&gt;
&lt;td&gt;1M/mo&lt;/td&gt;
&lt;td&gt;Lambda: 1M/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;D1: 5 GB, 5M reads/day&lt;/td&gt;
&lt;td&gt;Neon: 0.5 GB&lt;/td&gt;
&lt;td&gt;RDS: 20 GB (12-mo trial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV Storage&lt;/td&gt;
&lt;td&gt;1 GB, 100K reads/day&lt;/td&gt;
&lt;td&gt;Upstash: 256 MB, 16K cmd/day&lt;/td&gt;
&lt;td&gt;DynamoDB: 25 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Storage&lt;/td&gt;
&lt;td&gt;R2: 10 GB&lt;/td&gt;
&lt;td&gt;Blob: 1 GB&lt;/td&gt;
&lt;td&gt;S3: 5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0 (up to 100 GB)&lt;/td&gt;
&lt;td&gt;$0 (up to 100 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Paid tier
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;🟠 Cloudflare&lt;/th&gt;
&lt;th&gt;▲ Vercel&lt;/th&gt;
&lt;th&gt;☁️ AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base Price&lt;/td&gt;
&lt;td&gt;$5/mo&lt;/td&gt;
&lt;td&gt;$20/user/mo&lt;/td&gt;
&lt;td&gt;Pay-as-you-go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;10M/mo + $0.30/M&lt;/td&gt;
&lt;td&gt;10M/mo + $2/M&lt;/td&gt;
&lt;td&gt;$0.20/M requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;D1: 5GB + $0.75/GB&lt;/td&gt;
&lt;td&gt;Neon: $0.35/GB&lt;/td&gt;
&lt;td&gt;RDS: ~$13/mo + $0.115/GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV Storage&lt;/td&gt;
&lt;td&gt;$0.50/M reads, $5/M writes&lt;/td&gt;
&lt;td&gt;Upstash: $2/M commands&lt;/td&gt;
&lt;td&gt;$0.25/M reads, $1.25/M writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Storage&lt;/td&gt;
&lt;td&gt;R2: $0.015/GB/mo&lt;/td&gt;
&lt;td&gt;Blob: $0.023/GB/mo&lt;/td&gt;
&lt;td&gt;S3: $0.023/GB/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;1 TB/mo + $0.15/GB&lt;/td&gt;
&lt;td&gt;1 TB/mo + $0.09/GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two numbers do most of the work here. Egress on Cloudflare is always $0, so a viral moment never turns into a four figure bandwidth bill. And the database does not charge you a flat fee just for existing, the way a managed Postgres instance does. Pay for what you use, scale gradually, no idle tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for the AI era
&lt;/h2&gt;

&lt;p&gt;The way we write code has changed. AI agents like Cursor and Claude now write a large share of it. They are fast, but they struggle with architectural decisions and can quietly introduce security holes when they improvise on a generic template.&lt;/p&gt;

&lt;p&gt;A well structured, open codebase is the fix. PageZERO ships with an &lt;code&gt;AGENTS.md&lt;/code&gt; file that agents actually read. The moment an agent opens the project it understands the architecture, the conventions, and how to add a feature correctly. The module layout is predictable, every UI component follows the same four file pattern, and TypeScript is strict throughout, so an agent can reason about a function from its signature alone.&lt;/p&gt;

&lt;p&gt;PageZERO also bundles agent skills under &lt;code&gt;.agents/skills/&lt;/code&gt;. You can ask in plain language to scaffold a component, change the theme, open a pull request, or merge one, and the agent runs a structured workflow that follows the project conventions every time. You move fast with AI without trading away quality or security.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes wired together in the starter kit
&lt;/h2&gt;

&lt;p&gt;Each capability lives in its own module under &lt;code&gt;apps/&lt;/code&gt;, so you can keep it, remove it, or extend it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication.&lt;/strong&gt; Passwordless login with a six digit one time password sent by email. No passwords to store, no third party auth service, sessions in secure httpOnly cookies, and optional bot protection through Turnstile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments.&lt;/strong&gt; One time purchases and subscriptions through Polar.sh, including checkout, webhooks, invoicing, and global tax compliance. A completed payment grants the matching role automatically, and a refund or cancellation revokes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; Simple role based access control backed by the database and config, wired directly to payment events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email.&lt;/strong&gt; Resend integration with React Email and TailwindCSS templates, plus ready made templates for auth and access management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database.&lt;/strong&gt; Drizzle ORM on Cloudflare D1 with type safe queries, automatic migrations, and separate databases for production and preview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI.&lt;/strong&gt; 27 components built on Radix UI and TailwindCSS, accessible, documented in Storybook, with dark mode included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and delivery.&lt;/strong&gt; Vitest, Playwright, and Storybook preconfigured, with push to deploy through GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a weekend project cranked out to win a feature count. It is engineered to last, with unit and end to end tests included, strict TypeScript throughout, and a CI/CD pipeline that runs on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source matters here
&lt;/h2&gt;

&lt;p&gt;I made PageZERO fully open source because transparency is the strongest foundation you can build on. Most quality SaaS boilerplates are closed source and paywalled, so you rent someone else's architecture without ever seeing inside it. When every layer of your stack is open and community driven instead, you get the stability, security, and longevity that no proprietary shortcut can match. You can audit it, fork it, and trust it.&lt;/p&gt;

&lt;p&gt;There is a practical angle too. Asking an AI agent to generate a codebase of this quality from scratch can cost anywhere from $100 to $300 in tokens on a frontier model like Claude Opus, and the result is usually worse. PageZERO gives you a better starting point for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start building
&lt;/h2&gt;

&lt;p&gt;If any of this resonates, the fastest way to understand PageZERO is to run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx pagezero@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You only need &lt;a href="https://bun.com/" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; and &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pagezero.dev" rel="noopener noreferrer"&gt;pagezero.dev&lt;/a&gt;&lt;/strong&gt; - the project homepage with an overview of everything included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.pagezero.dev/getting-started/quick-start/" rel="noopener noreferrer"&gt;docs.pagezero.dev&lt;/a&gt;&lt;/strong&gt; - comprehensive documentation covering setup, architecture, every module, and how to extend the kit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/pagezero-dev/pagezero" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt; - the full source under MIT, read every line before you commit to it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build your startup on solid foundations. Start free, scale for pennies, own every line.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>saas</category>
      <category>cloudflare</category>
    </item>
  </channel>
</rss>
