Cloudflare Workers est devenu en 2026 l’option par défaut pour héberger une application TanStack Start moderne sans payer un VPS dédié. Le PoP local d’Abidjan, Lagos ou Johannesburg sert le HTML en moins de 100 ms à un visiteur d’Afrique de l’Ouest, le quota gratuit couvre 100 000 requêtes par jour, et les bindings KV, D1 et R2 sont accessibles directement depuis vos server functions sans client externe. Ce tutoriel construit un déploiement complet, depuis la création du projet jusqu’à la première requête SSR servie par un Worker, avec stockage de données dans D1 et upload de fichiers dans R2.
Article de la série autour de TanStack Start en production 2026.
Prérequis
Avant d’attaquer, vérifiez les éléments suivants. Le tutoriel suppose une connaissance basique de TanStack Start. Si ce n’est pas le cas, lisez d’abord le tutoriel d’installation TanStack Start.
- Un compte Cloudflare (le tier gratuit suffit pour ce tutoriel).
- Node.js 22 LTS et npm 10 ou plus récent.
- Wrangler CLI :
npm install -g wranglerpuiswrangler login. - Connaissances basiques de SSR et des server functions TanStack Start.
- Environ 45 minutes pour suivre toutes les étapes jusqu’au déploiement.
Étape 1 — Créer le projet avec le template officiel Cloudflare
La méthode la plus directe en 2026 est la CLI create-cloudflare qui scaffolde un projet TanStack Start préconfiguré pour Workers : plugins Vite déjà alignés, wrangler.jsonc généré, scripts npm deploy et preview prêts. Cela évite plusieurs heures de bidouillage manuel pour faire cohabiter les plugins.
npm create cloudflare@latest -- my-tanstack-app --framework=tanstack-start
cd my-tanstack-app
npm run dev
L’outil pose deux questions, accepte par défaut TypeScript et Git, puis installe tout. Au lancement de npm run dev, l’URL http://localhost:5173 doit servir une page d’accueil rendue côté serveur. Si la page apparaît, la chaîne Vite + TanStack Start + plugin Cloudflare fonctionne correctement en local.
Étape 2 — Lire le vite.config.ts généré
Le fichier de config est l’élément critique du déploiement. Trois plugins doivent cohabiter dans un ordre précis : cloudflare() du paquet officiel @cloudflare/vite-plugin, tanstackStart() du paquet @tanstack/react-start, et react(). Le scaffolder pose la bonne config, mais il est utile de comprendre ce qui s’y joue.
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tanstackStart(),
react(),
],
})
L’option viteEnvironment: { name: 'ssr' } indique au plugin Cloudflare qu’il doit cibler l’environnement SSR de TanStack Start. Sans cette option, le bundler générerait deux Workers distincts, ce qui n’est pas ce qu’on veut. L’ordre des plugins est aussi important : cloudflare en premier, sinon les transformations de TanStack Start ne sont pas compatibles avec le runtime Workers.
Étape 3 — Configurer wrangler.jsonc
Wrangler est l’outil officiel Cloudflare pour build et déployer les Workers. Sa configuration vit dans wrangler.jsonc à la racine du projet. Le fichier généré par la CLI contient déjà l’essentiel ; voici la version commentée pour comprendre chaque ligne.
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-tanstack-app",
"compatibility_date": "2026-05-06",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"observability": { "enabled": true }
}
Trois champs comptent particulièrement. compatibility_date verrouille le runtime Workers à la date donnée : changer cette date peut activer ou désactiver des comportements. compatibility_flags: ["nodejs_compat"] active la compatibilité Node.js, indispensable pour que les paquets utilisant node:buffer ou node:crypto tournent. main pointe vers l’entrée serveur de TanStack Start, qui est résolue automatiquement par le plugin.
Étape 4 — Créer un binding KV pour le cache d’auth
Cloudflare KV est un store clé-valeur global et eventually consistent, parfait pour des sessions, des feature flags ou un cache léger. On le crée d’abord avec la CLI Wrangler, puis on l’attache au projet via wrangler.jsonc.
wrangler kv namespace create "SESSION_CACHE"
La commande imprime un ID de namespace au format UUID. Copiez-le, ajoutez la section kv_namespaces à votre wrangler.jsonc avec le binding SESSION_CACHE et l’ID retourné. Une fois fait, ce binding sera accessible côté code via env.SESSION_CACHE.
// wrangler.jsonc (extrait avec KV)
{
"name": "my-tanstack-app",
"compatibility_date": "2026-05-06",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"kv_namespaces": [
{
"binding": "SESSION_CACHE",
"id": "votre-id-uuid-ici"
}
]
}
Le binding ne consomme rien tant qu’il n’est pas utilisé : pas de surcoût d’avoir le namespace déclaré. C’est aussi pourquoi on en crée généralement plusieurs au début (sessions, cache HTTP, feature flags) plutôt que d’éditer le fichier à chaque ajout.
Étape 5 — Lire un binding dans une server function
Les bindings ne sont pas accessibles via process.env sur Workers — ils vivent dans un objet env spécifique. TanStack Start expose ce env via le module virtuel cloudflare:workers qui est résolu uniquement au build par le plugin Cloudflare. Côté code, ça ressemble à un import standard.
// app/server/session.ts
import { createServerFn } from '@tanstack/react-start'
import { env } from 'cloudflare:workers'
export const getSession = createServerFn({ method: 'GET' }).handler(async () => {
const value = await env.SESSION_CACHE.get('current-user')
return value ? JSON.parse(value) : null
})
export const setSession = createServerFn({ method: 'POST' }).handler(
async ({ data }: { data: { user: string } }) => {
await env.SESSION_CACHE.put(
'current-user',
JSON.stringify(data),
{ expirationTtl: 60 * 60 * 24 }, // 24 h
)
return { ok: true }
},
)
Au build, le plugin Cloudflare remplace cloudflare:workers par le vrai objet env du Worker. En local pendant npm run dev, Wrangler simule le binding KV via Miniflare, donc vous pouvez tester sans déployer. Le signal de réussite : un GET / qui appelle getSession retourne null au premier accès, puis la valeur JSON après un setSession.
Étape 6 — Brancher une base D1 pour la persistance
D1 est la base SQLite distribuée de Cloudflare. Elle convient parfaitement à 80 % des applications Start : transactions ACID, syntaxe SQL standard, latence faible. On la crée avec Wrangler, on déclare la migration initiale, et on attache le binding.
wrangler d1 create my-tanstack-db
# imprime un database_id à copier dans wrangler.jsonc
Ajoutez ensuite le binding D1 dans wrangler.jsonc avec le nom DB par convention. La structure est similaire à KV mais avec une section d1_databases.
// wrangler.jsonc (avec D1)
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-tanstack-db",
"database_id": "votre-database-id"
}
]
}
Pour exécuter une migration locale ou en production, utilisez wrangler d1 execute. Le code ci-dessous crée une table posts.
echo "CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT);" > migrations/0001_init.sql
wrangler d1 execute my-tanstack-db --local --file=migrations/0001_init.sql
wrangler d1 execute my-tanstack-db --remote --file=migrations/0001_init.sql
Le flag --local agit sur la base locale Miniflare, --remote sur la production. Les deux doivent rester synchronisées. Pour gérer les migrations sérieusement, l’idiome 2026 consiste à utiliser Drizzle dans les server functions avec son adapter D1, qui génère et applique les migrations à partir d’un schéma TypeScript.
Étape 7 — Lire et écrire dans D1 depuis une server function
Le binding env.DB expose une API D1 native avec préparation de requêtes et liaison de paramètres. Pas d’ORM nécessaire pour démarrer ; pour une vraie application, plug Drizzle au-dessus.
// app/server/posts.ts
import { createServerFn } from '@tanstack/react-start'
import { env } from 'cloudflare:workers'
export const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const { results } = await env.DB
.prepare('SELECT id, title, body FROM posts ORDER BY id DESC')
.all<{ id: number; title: string; body: string }>()
return results
})
export const createPost = createServerFn({ method: 'POST' }).handler(
async ({ data }: { data: { title: string; body: string } }) => {
const result = await env.DB
.prepare('INSERT INTO posts (title, body) VALUES (?, ?) RETURNING id')
.bind(data.title, data.body)
.first<{ id: number }>()
return { id: result?.id }
},
)
L’API prepare(...).bind(...).all() est la version sécurisée — toujours passer par bind pour éviter les injections SQL. Côté composant, on appelle ces server functions exactement comme dans le tutoriel sur les server functions. Aucune différence pour le code React : la couche D1 est invisible côté client.
Étape 8 — Stocker des fichiers dans un bucket R2
R2 est le service S3-compatible de Cloudflare, sans frais de bande passante sortante. On crée un bucket avec Wrangler, on l’attache au projet et on l’utilise depuis une server function pour uploader un fichier.
wrangler r2 bucket create avatars-bucket
Une fois créé, on l’ajoute à wrangler.jsonc via la section r2_buckets.
// wrangler.jsonc (avec R2)
{
"r2_buckets": [
{
"binding": "AVATARS",
"bucket_name": "avatars-bucket"
}
]
}
L’API env.AVATARS.put() accepte un ArrayBuffer, une ReadableStream ou un string. Voici un exemple d’upload depuis une server function.
// app/server/upload.ts
import { createServerFn } from '@tanstack/react-start'
import { env } from 'cloudflare:workers'
export const uploadAvatar = createServerFn({ method: 'POST' })
.inputValidator((input: unknown) => {
if (!(input instanceof FormData)) throw new Error('FormData attendu')
const file = input.get('file')
if (!(file instanceof File)) throw new Error('Champ file manquant')
return { file }
})
.handler(async ({ data }) => {
const key = `avatars/${crypto.randomUUID()}.${data.file.type.split('/')[1]}`
await env.AVATARS.put(key, data.file.stream(), {
httpMetadata: { contentType: data.file.type },
})
return { key }
})
L’objet est stocké, vous récupérez sa clé pour la réutiliser dans une URL signée ou un domaine custom. Pour servir le fichier publiquement, il faut soit attacher un domaine custom au bucket, soit servir via une route dédiée qui appelle env.AVATARS.get(key) et streame la réponse.
Étape 9 — Déployer en production
Avec les bindings configurés et le code testé en local, le déploiement se résume à deux commandes. Le build produit un Worker prêt à l’emploi, et wrangler deploy le pousse vers Cloudflare en quelques secondes.
npm run build
npm run deploy
La commande deploy imprime l’URL https://my-tanstack-app.<votre-compte>.workers.dev. Ouvrez-la dans un navigateur, vous devez voir votre page rendue côté serveur. Pour un domaine custom (par exemple app.exemple.com), allez dans le dashboard Cloudflare et associez-le à ce Worker via la section Routes du tableau de bord Workers.
Étape 10 — Vérifier les logs et les métriques
Une fois en production, l’observabilité passe par wrangler tail (logs en streaming) et le dashboard Cloudflare (métriques agrégées). Activez observability: { enabled: true } dans wrangler.jsonc pour bénéficier des Logs persistants pendant 7 jours.
wrangler tail my-tanstack-app --format pretty
La commande affiche en temps réel chaque requête HTTP avec son code de retour et son console.log. C’est l’outil de debug le plus efficace pour comprendre pourquoi une server function lève en production. Pour des erreurs structurées, intégrez Sentry via le SDK Cloudflare Workers, mais pour 90 % des projets, tail suffit largement.
Différences runtime à connaître
Le runtime Workers n’est pas Node.js. Quelques différences importantes peuvent surprendre.
| Aspect | Node.js | Workers |
|---|---|---|
| API fichier | fs disponible |
Pas de système de fichiers, utiliser R2 |
| Modules natifs | better-sqlite3, etc. |
Non supportés, utiliser D1 |
| process.env | Standard | Variables via env.* du binding |
| Limite CPU | Aucune | 50 ms gratuit, 30 s payant |
| Limite mémoire | Du serveur | 128 Mo par invocation |
| Démarrage à froid | Variable | Quasi nul (V8 isolates) |
| WebSocket | Standard | Via Durable Objects |
La majorité des paquets npm récents supportent les deux runtimes via la condition workerd dans leur package.json. Quand un paquet ne fonctionne pas, c’est généralement parce qu’il dépend de fs ou de modules natifs. Cherchez une alternative ou ouvrez une issue ; en 2026 l’écosystème est nettement plus mature qu’il y a deux ans.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
Cannot resolve "node:fs" |
Dépendance Node-only dans le bundle | Activer nodejs_compat ou trouver une alternative |
env.MY_KV is undefined |
Binding non déclaré dans wrangler.jsonc |
Vérifier la section kv_namespaces et l’ID |
| Build OK mais 404 partout | main mal défini dans Wrangler |
Pointer vers @tanstack/react-start/server-entry |
| HTML statique uniquement | Plugin Cloudflare manquant | Ajouter cloudflare() dans vite.config.ts |
| Migrations D1 manquent en prod | Oubli du flag --remote |
Rejouer wrangler d1 execute --remote |
| Limite CPU dépassée | Calcul lourd dans le handler | Déléguer à un Durable Object ou un service externe |
FAQ
Le tier gratuit suffit-il pour démarrer ? Oui, pour 100 000 requêtes par jour, 1 Go de KV, 5 Go de D1 et 10 Go de R2. Au-delà, le plan Workers Paid à environ 5 USD/mois couvre largement plusieurs milliers d’utilisateurs actifs.
Peut-on connecter une base PostgreSQL externe (Neon, Supabase) ? Oui, via Hyperdrive (le proxy de Cloudflare) ou directement via TCP en utilisant le Connect API. D1 reste plus simple et moins chère pour la majorité des cas.
Comment gérer les variables d’environnement secrètes ? wrangler secret put MY_SECRET stocke le secret côté Cloudflare, accessible via env.MY_SECRET. Ne jamais commiter de secrets dans wrangler.jsonc.
Le build est-il long ? Sur un projet TanStack Start moyen, npm run build tourne en 15 à 30 secondes, et wrangler deploy en 5 à 10 secondes supplémentaires. La propagation globale est immédiate.
Comment rollback en cas de problème ? Cloudflare conserve les versions précédentes. Allez dans le dashboard Workers, onglet Deployments, et promouvez une version antérieure. C’est instantané.
Pour aller plus loin
- L’article principal : TanStack Start en production 2026.
- Pour utiliser Drizzle avec D1 : server functions TanStack Start.
- Pour brancher l’authentification : better-auth dans TanStack Start.
- Pour la couche données côté SSR : TanStack Query SSR et hydratation.
Ressources
- Documentation officielle Cloudflare pour TanStack Start : developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start
- Plugin Cloudflare Vite : developers.cloudflare.com/workers/vite-plugin
- Documentation D1 : developers.cloudflare.com/d1
- Documentation R2 : developers.cloudflare.com/r2
- Wrangler CLI : developers.cloudflare.com/workers/wrangler