aller au contenu
FRFlo
Table des matières

Concevoir une application web moderne impose souvent de faire des arbitrages : complexité des frameworks full-stack, latence d’accès aux bases de données centralisées, ou lourdeur des infrastructures serveur traditionnelles.

Pour mes projets, j’ai conçu un template d’architecture complet combinant le meilleur de l’écosystème frontend et de l’edge computing serverless : SvelteKit 5 s’exécutant directement sur Cloudflare Workers, avec une persistance relationnelle distribuée (Cloudflare D1), une authentification native (Better Auth), une gestion d’effets typée (Effect) et un outillage ultra-rapide motorisé par Bun.

Ce retour d’expérience détaille l’anatomie de cette architecture de production, ses choix techniques et les pièges classiques à éviter lors du déploiement sur les isolates V8 de Cloudflare.


1. Vue d’ensemble de la Stack

L’architecture repose sur une chaîne technique moderne et cohérente où chaque couche répond à un rôle précis sans dépendance superflue.

flowchart TB
    subgraph Client["Navigateur Client"]
        BrowserUI["Svelte 5 Runes ($state, $derived)"]
        AuthClient["Better Auth Svelte Client (#lib/auth-client)"]
        TailwindUI["Tailwind CSS 4 (Oxide Engine)"]
    end

    subgraph EdgeWorker["Cloudflare Workers Runtime (workerd)"]
        subgraph SvelteKitCore["SvelteKit 5 (@sveltejs/adapter-cloudflare)"]
            HooksServer["hooks.server.ts<br>(Paraglide i18n + handleAuth)"]
            RoutesServer["Pages & Endpoints (+page.svelte, /api/auth)"]
        end

        subgraph RuntimeEffect["Couche Métier Effect"]
            EnvService["CloudflareEnvironment (Context.Service)"]
            Runner["runWithCloudflareEnv (Isolation Request-Scoped)"]
        end
    end

    subgraph EdgeData["Persistance & Assets Cloudflare"]
        D1Native["Cloudflare D1 (SQLite Edge Database)"]
        StaticAssets["Workers Static Assets Binding (ASSETS)"]
    end

    BrowserUI -->|Requête HTTP / SSR| HooksServer
    HooksServer --> RoutesServer
    RoutesServer -->|Injection ponctuelle env| RuntimeEffect
    RuntimeEffect -->|d1Native direct| D1Native
    HooksServer -.->|Contrôle de session| RuntimeEffect
    RoutesServer -.->|Fichiers statiques & SSR| StaticAssets
    AuthClient -->|API REST /api/auth/*| RoutesServer

Les Briques Fondamentales

  • Frontend & Rendu : SvelteKit 5 avec le nouveau modèle de runes, compilé pour le runtime workerd avec isolation de l’application dans le groupe de routes (app).
  • Exécution Edge : Isolates V8 Cloudflare Workers, démarrage à froid quasi-nul (< 5 ms) et scaling mondial automatique.
  • Base de Données : Cloudflare D1 (SQLite distribué à l’edge) sans allers-retours réseau vers un serveur central.
  • Authentification : Better Auth couplé au plugin better-auth-cloudflare pour une intégration D1 sans couche ORM lourde.
  • Fiabilité & Contexte : Effect pour encapsuler les liaisons Cloudflare propres à chaque requête et fiabiliser la gestion d’erreurs.
  • Internationalisation : Paraglide JS avec compilation anticipée zéro-runtime.
  • Style & Design System : Tailwind CSS v4 via @tailwindcss/vite, primitives UI shadcn-svelte (bits-ui), animations tailwindcss-animate, tokens neutres (DESIGN.md) et icônes Lucide Svelte.
  • Configuration & Craft IA : Protocole interactif de setup assisté par IA (SETUP.md) et règles d’artisanat UI / motion pour les agents de code (AGENTS.md).
  • Qualité & Tests : Bun, Oxlint (enrichi du plugin @shadcn/lint), Oxfmt, Vitest et Playwright (validation E2E des fonctionnalités en conditions réelles).

2. Le Cœur Runtime : SvelteKit 5 sur Cloudflare Workers

Déployer SvelteKit sur Cloudflare Workers ne consiste pas à exécuter un serveur Node.js classique dans un conteneur. Le code tourne directement au sein des isolates V8 gérés par le moteur open source workerd.

A. Configuration de l’Adaptateur

L’adaptateur officiel @sveltejs/adapter-cloudflare génère un point d’entrée unique .svelte-kit/cloudflare/_worker.js.

La configuration dans wrangler.jsonc définit les liaisons essentielles :

{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "template-cloudflare-svelte",
"compatibility_date": "2026-10-20",
"compatibility_flags": ["nodejs_als"],
"main": ".svelte-kit/cloudflare/_worker.js",
"assets": {
"binding": "ASSETS",
"directory": ".svelte-kit/cloudflare",
},
"d1_databases": [
{
"binding": "DATABASE",
"database_name": "template-cloudflare-svelte",
"database_id": "00000000-0000-0000-0000-000000000000",
"migrations_dir": "migrations",
},
],
}

B. Le Flag nodejs_als

Le flag de compatibilité nodejs_als (AsyncLocalStorage) est crucial. Il permet aux bibliothèques modernes de propager des contextes asynchrones à travers la chaîne d’appels de manière fiable sur les Workers, sans nécessiter l’ensemble des polyfills Node.js.

C. Assets Statiques Découplés

La directive assets délègue le service des fichiers statiques (JavaScript compilé, images, polices, robots.txt, sitemap) directement au CDN Cloudflare via la liaison ASSETS. Le code applicatif de votre Worker n’est exécuté que pour le SSR dynamique ou les routes d’API, économisant du CPU et réduisant les coûts.


3. L’Intégration d’Effect : Dompter les Liaisons Request-Scoped

L’une des particularités les plus piégeuses de l’architecture Cloudflare Workers réside dans le cycle de vie de ses liaisons (bindings).

Le Piège du Singleton Global

Dans un serveur Node classique, on initialise souvent la connexion à la base de données une fois au démarrage de l’application :

// ANTI-PATTERN SUR CLOUDFLARE WORKERS :
const db = createDatabase(process.env.DATABASE_URL);

Sur Cloudflare Workers, l’objet env (qui contient la référence à votre base D1, les secrets et les variables d’environnement) n’existe pas au niveau global. Il est injecté dynamiquement par le runtime à chaque requête HTTP dans l’objet platform.env.

Conserver une référence globale vers env ou instancier un client persistant en dehors du cycle d’une requête conduit inévitablement à des fuites de mémoire, des données croisées entre utilisateurs ou des erreurs de contexte expiré (“Cannot perform I/O on behalf of a different request”).

La Solution avec Effect

Pour structurer proprement ces dépendances sans passer manuellement env d’argument en argument, le template intègre Effect :

src/lib/server/effect.ts
import { Context, Effect } from "effect";
export type CloudflareEnv = Env & {
BETTER_AUTH_SECRET: string;
BETTER_AUTH_URL: string;
};
// Déclaration d'un service contextuel typé
export class CloudflareEnvironment extends Context.Service<
CloudflareEnvironment,
CloudflareEnv
>()("app/CloudflareEnvironment") {}
// Exécuteur sécurisé borné à la requête
export function runWithCloudflareEnv<A, E>(
env: CloudflareEnv,
program: Effect.Effect<A, E, CloudflareEnvironment>,
): Promise<A> {
return Effect.runPromise(
Effect.provideService(program, CloudflareEnvironment, env),
);
}

Consommation dans les Endpoints

Dans le point d’entrée d’API Better Auth (src/routes/api/auth/[...all]/+server.ts), la requête est traitée sous forme d’Effect avec gestion stricte des erreurs :

const handle: RequestHandler = async ({ request, platform }) => {
if (!platform?.env) {
return new Response("Cloudflare environment is unavailable", {
status: 500,
});
}
const program = Effect.catch(handleAuthRequest(request), (error) => {
console.error("Auth error:", error);
return Effect.succeed(
new Response("Internal Server Error", { status: 500 }),
);
});
return runWithCloudflareEnv(platform.env, program);
};

Cette approche apporte trois garanties fondamentales :

  1. Étanchéité totale : aucun état partagé entre requêtes distinctes.
  2. Typage strict : les exigences en environnement sont formalisées dans la signature des types Effect.
  3. Résilience : les erreurs attendues sont traitées de manière explicite sans risque de crash inattendu de l’isolate.

4. Persistance Distribuée : Cloudflare D1 & Migrations Déclaratives

Cloudflare D1 fournit une base de données relationnelle SQLite distribuée directement sur le réseau edge de Cloudflare.

flowchart LR
    Dev["Développeur / CI"] -->|wrangler d1 migrations apply| LocalDB[".wrangler/state/v3/d1 (D1 Local)"]
    Dev -->|wrangler d1 migrations apply --remote| RemoteDB["Cloudflare D1 (Production Edge)"]
    Worker["Worker SSR / API"] -->|Binding SQL direct| RemoteDB

A. Pourquoi SQLite à l’Edge ?

Dans une architecture edge où le code s’exécute à quelques millisecondes de l’utilisateur, interroger un PostgreSQL ou un MySQL distant réintroduit une latence réseau de 100 à 200 ms par requête. D1 exécute les lectures au plus près du Worker, éliminant ce goulot d’étranglement.

B. Gestion des Migrations

Les évolutions de schéma sont stockées dans le répertoire migrations/ sous forme de fichiers SQL numérotés (ex. 0001_better_auth.sql).

Le workflow est totalement prédictible :

Terminal window
# Application sur la base SQLite locale de développement
bunx wrangler d1 migrations apply template-cloudflare-svelte --local
# Application sur la base distante en production
bunx wrangler d1 migrations apply template-cloudflare-svelte --remote

5. Authentification à l’Edge : Better Auth & Propagation de Session

L’authentification est souvent le point noir des architectures serverless. Ce template s’appuie sur Better Auth avec son adaptateur better-auth-cloudflare.

A. Intégration Native D1

Au lieu d’utiliser un ORM intermédiaire lourd, Better Auth dialogue directement avec la liaison D1 via d1Native :

src/lib/server/auth.ts
export function createAuth(env: CloudflareEnv) {
return betterAuth({
...withCloudflare(
{
d1Native: env.DATABASE,
autoDetectIpAddress: true,
geolocationTracking: false,
},
{
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
emailAndPassword: { enabled: true },
},
),
});
}

B. Propagation de Session dans SvelteKit (hooks.server.ts)

Pour que la session soit accessible partout côté serveur (+page.server.ts, +layout.server.ts), le template extrait et injecte la session dans event.locals à chaque requête :

src/hooks.server.ts
const handleAuth: Handle = async ({ event, resolve }) => {
if (!event.platform?.env) {
event.locals.session = null;
event.locals.user = null;
return resolve(event);
}
const sessionProgram = Effect.catch(getSession(event.request), () =>
Effect.succeed(null),
);
const sessionData = await runWithCloudflareEnv(
event.platform.env,
sessionProgram,
);
event.locals.session = sessionData?.session ?? null;
event.locals.user = sessionData?.user ?? null;
return resolve(event);
};
export const handle: Handle = sequence(handleParaglide, handleAuth);

Et dans src/app.d.ts, les types sont immédiatement disponibles :

import type { Session, User } from "better-auth";
declare global {
namespace App {
interface Locals {
session: Session | null;
user: User | null;
}
}
}

C. Client Frontend Isomorphe

Côté client, le helper src/lib/auth-client.ts exporte le client Svelte :

import { createAuthClient } from "better-auth/svelte";
export const authClient = createAuthClient();

Il permet de gérer la connexion, l’inscription et la déconnexion avec réactivité native et gestion automatique des cookies de session chiffrés.


6. Internationalisation, Style & Design System

A. Paraglide JS : i18n Compilée sans Surcharge

Plutôt que des bibliothèques i18n traditionnelles qui embarquent de lourds dictionnaires JSON analysés au runtime, Paraglide JS compile les traductions sous forme de fonctions TypeScript ultra-optimisées :

  • Zéro surcoût d’analyse JSON au runtime.
  • Autocomplétion et typage strict des messages (m.app_title()).
  • Directives d’écriture et de langue injectées automatiquement dans les balises HTML via transformPageChunk.
  • Support des balises d’alternatives SEO (<link rel="alternate" hreflang="...">) et d’un endpoint dynamique /sitemap.xml.

B. Tailwind CSS 4, Moteur Oxide & Animations

Le template adopte Tailwind CSS 4 propulsé par son nouveau moteur Oxide écrit en Rust :

  • Configuration directe via CSS (@import "tailwindcss"; @plugin "@tailwindcss/forms";).
  • Intégration Vite ultra-rapide sans fichier de configuration JavaScript encombrant.
  • Animations fluides via @plugin "tailwindcss-animate" configuré directement dans src/routes/layout.css.
  • Formatage automatique des classes CSS via Oxfmt.

C. Primitives UI (shadcn-svelte & bits-ui) à la Demande : Zéro Boilerplate

Dans de nombreux templates de démarrage, l’arborescence UI se retrouve surchargée de dizaines de composants pré-générés (boutons, boîtes de dialogue, formulaires) dont une grande partie ne sera jamais utilisée.

Le template prend le parti inverse : l’écosystème de composants à la demande sans bruit inutile :

  • Configuration native components.json : L’environnement est prêt pour la CLI shadcn-svelte, pointant vers les primitives accessibles et non stylées de bits-ui.
  • Utilitaire cn() mutualisé : Le fichier src/lib/utils.ts configure l’agrégation propre de classes via clsx et tailwind-merge.
  • Zéro composant mort : Aucun composant pré-généré n’est commité par défaut dans src/lib/components/ui/. Les composants sont instanciés ou créés chirurgicalement par le développeur ou l’agent selon les besoins réels du produit.
  • Audit de conformité : Oxlint est enrichi de la règle @shadcn/lint pour s’assurer que chaque composant UI ajouté respecte la structure attendue.

D. Design System Déclaratif (DESIGN.md) et Règles Craft UI (AGENTS.md)

Pour qu’un template soit immédiatement appropriable par un humain ou par un agent IA, l’identité visuelle doit être documentée de manière neutre et standardisée :

  • Spécification des Tokens : Les fichiers DESIGN.md et .impeccable/design.json définissent la hiérarchie sémantique des couleurs (surfaces, texte, bordures, accents), la typographie et les arrondis de bordures.
  • Règles de Craft & Motion : Le document AGENTS.md formalise des exigences de qualité d’interface :
    • Mouvement intentionnel : Chaque transition doit avoir un but fonctionnel clair et demeurer interrompable.
    • Performance 60 fps : Priorité aux animations transform/opacity déléguées au compositeur GPU.
    • Accessibilité universelle : Navigation complète au clavier, contrastes renforcés et respect strict de la directive prefers-reduced-motion.

E. Protocole de Configuration Assisté par IA (SETUP.md)

Les agents de code autonomes sont devenus des acteurs centraux de la création logicielle. Le fichier SETUP.md formalise un protocole interactif de configuration :

Lorsqu’un agent IA est mandaté pour démarrer un nouveau projet à partir du template, il déroule un formulaire interactif en 4 questions clés :

  1. Identité : Nom de l’application, description, domaine cible.
  2. Palette & Ambiance : Thème minimaliste sombre ou charte de marque personnalisée.
  3. i18n : Langue par défaut et locales supportées par Paraglide.
  4. Auth & Base D1 : Fournisseurs Better Auth (Email/MDP, OAuth GitHub/Google) et nom des bases Cloudflare D1.

L’agent applique ensuite l’intégralité des ajustements de code et de configuration en une seule passe, livrant un projet opérationnel sans friction.


7. Résolution des Défis de Typage et Pipeline CI/CD

La mise en place de cette architecture a mis en lumière un problème classique mais critique sur Cloudflare Workers : l’interdépendance des types et du build.

L’Élimination du Versionnement de worker-configuration.d.ts

Initialement, le fichier de définitions worker-configuration.d.ts généré par wrangler types (dépassant 16 000 lignes) était conservé dans l’arborescence Git. Cette approche créait plusieurs frictions majeures :

  • Pollution de l’historique Git : Chaque régénération locale ou modification mineure de configuration réécrivait des milliers de lignes de types générés.
  • Le paradoxe du point d’entrée : Dans wrangler.jsonc, le point d’entrée déclaré est "main": ".svelte-kit/cloudflare/_worker.js". Sur un clone vierge (notamment en CI), ce fichier compilé n’existe pas encore au démarrage. Lancer wrangler types --check avant la compilation échouait systématiquement faute de retrouver mainModule.

Le template résout ce problème à la racine en retirant worker-configuration.d.ts du suivi Git (ajouté au .gitignore).

Découplage de la Génération des Types (Local & CI)

La génération des types est désormais orchestrée selon deux modes complémentaires :

  1. En développement local : Le script dev déclenche la vérification et l’alignement des types avant de démarrer Vite :

    "dev": "wrangler types --check && vite dev",
    "gen": "wrangler types"

    La commande bun run gen reste disponible pour rafraîchir explicitement les définitions TypeScript à la demande.

  2. Dans le pipeline GitHub Actions (CI) : Le build compile d’abord l’application SvelteKit via Vite, puis produit les types directement sur l’environnement de build :

    - name: Run build
    run: bun run build
    - name: Generate Wrangler types
    run: bun run gen
    - name: Type check
    run: bun run check

Dans package.json, le script de compilation redevient prédictible et épuré :

"build": "bun run paraglide:compile && svelte-kit sync && vite build"

La Suite de Validation Complète

La validation automatisée s’exécute en moins de deux minutes sous GitHub Actions :

  • bun run lint : Oxlint pour un linting statique instantané, avec le plugin @shadcn/lint pour auditer la structure des composants.
  • bun run format:check : Oxfmt garantissant la rigueur de formatage du code et du markdown.
  • bun run build & bun run check : Validation des types SvelteKit et Wrangler.
  • bun run test:unit : Tests unitaires Vitest de l’environnement Effect.
  • bun run test:e2e : Tests de bout en bout Playwright validant l’application servie par le Worker local (wrangler dev), avec assertions explicites sur les fonctionnalités de la page d’accueil.

8. Conclusion

Cette architecture SvelteKit 5 + Cloudflare Workers élimine le fossé historique entre la complexité de gestion d’une infrastructure serveur et les besoins de performance d’une application moderne.

En combinant :

  • La réactivité fluide de SvelteKit 5,
  • L’isolation ultra-rapide des V8 Isolates,
  • La persistance SQL distribuée de Cloudflare D1,
  • L’encapsulation fonctionnelle robuste d’Effect,
  • Et un outillage moderne (Bun, Oxlint, Better Auth),

ce socle offre une base solide, rapide et prête pour la production.

Le code source complet de cette architecture est disponible sur GitHub : 👉 FRFlo/template-cloudflare-svelte