aller au contenu
FRFlo
Table des matières

Designing a modern web application often forces hard trade-offs: full-stack framework complexity, network latency to centralized databases, or heavy traditional server management.

For my projects, I designed a production template combining the best of the frontend ecosystem and serverless edge computing: SvelteKit 5 running directly on Cloudflare Workers, backed by distributed relational persistence (Cloudflare D1), native authentication (Better Auth), typed effect management (Effect), and an ultra-fast toolchain powered by Bun.

This engineering breakdown details the anatomy of this production architecture, its design choices, and common pitfalls to avoid when deploying on Cloudflare’s V8 isolates.


1. Stack Architecture Overview

The architecture relies on a coherent, modern toolchain where each layer fulfills a specific purpose without unnecessary dependencies.

flowchart TB
    subgraph Client["Client Browser"]
        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["Effect Business Layer"]
            EnvService["CloudflareEnvironment (Context.Service)"]
            Runner["runWithCloudflareEnv (Request-Scoped Isolation)"]
        end
    end

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

    BrowserUI -->|HTTP Request / SSR| HooksServer
    HooksServer --> RoutesServer
    RoutesServer -->|Per-request env injection| RuntimeEffect
    RuntimeEffect -->|Direct d1Native| D1Native
    HooksServer -.->|Session verification| RuntimeEffect
    RoutesServer -.->|Static files & SSR| StaticAssets
    AuthClient -->|REST API /api/auth/*| RoutesServer

Core Building Blocks

  • Frontend & Rendering: SvelteKit 5 using runes reactivity, compiled for the workerd runtime with app isolation inside the (app) route group.
  • Edge Execution: Cloudflare Workers V8 isolates with near-instant cold starts (< 5 ms) and automatic global scaling.
  • Database: Cloudflare D1 (distributed edge SQLite) eliminating network round-trips to distant origin databases.
  • Authentication: Better Auth paired with better-auth-cloudflare for native D1 integration without heavy ORM overhead.
  • Reliability & Context: Effect for request-scoped environment binding isolation and typed functional error handling.
  • Internationalization: Paraglide JS with compile-time zero-runtime message resolution.
  • Styling & Design System: Tailwind CSS v4 via @tailwindcss/vite, shadcn-svelte UI primitives (bits-ui), tailwindcss-animate, neutral design tokens (DESIGN.md), and Lucide Svelte icons.
  • AI-Assisted Setup & Craft: Interactive AI onboarding protocol (SETUP.md) and UI craft & motion guidelines for coding agents (AGENTS.md).
  • Quality & Verification: Bun, Oxlint (enhanced with @shadcn/lint), Oxfmt, Vitest, and Playwright (end-to-end feature validation in real conditions).

2. The Runtime Core: SvelteKit 5 on Cloudflare Workers

Deploying SvelteKit on Cloudflare Workers is fundamentally different from running Node.js in a container. The code executes directly inside V8 isolates powered by the open-source workerd engine.

A. Adapter Configuration

The official @sveltejs/adapter-cloudflare produces a single entrypoint bundle at .svelte-kit/cloudflare/_worker.js.

The wrangler.jsonc file configures key bindings:

{
"$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. The nodejs_als Compatibility Flag

The nodejs_als (AsyncLocalStorage) flag is critical. It enables modern libraries to propagate asynchronous execution contexts across call stacks on Workers without requiring full Node.js polyfill layers.

C. Decoupled Static Assets

The assets directive routes static file requests (compiled JS bundles, images, fonts, robots.txt, sitemap) directly through the Cloudflare CDN using the ASSETS binding. Worker CPU time is only consumed for dynamic SSR rendering or API requests.


3. Effect Integration: Taming Request-Scoped Bindings

One of the most common pitfalls on Cloudflare Workers stems from binding lifecycles.

The Global Singleton Anti-Pattern

In traditional Node.js servers, developers often instantiate database clients once at startup:

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

On Cloudflare Workers, the env object (containing D1 database handles, secrets, and environment variables) does not exist globally. It is injected dynamically by the runtime per HTTP request inside platform.env.

Holding a global reference to env or initializing persistent clients outside the request scope causes memory leaks, cross-request state pollution, and runtime errors (“Cannot perform I/O on behalf of a different request”).

The Effect Solution

To manage these dependencies cleanly without threading env manually through every function, the template uses Effect:

src/lib/server/effect.ts
import { Context, Effect } from "effect";
export type CloudflareEnv = Env & {
BETTER_AUTH_SECRET: string;
BETTER_AUTH_URL: string;
};
// Define a typed contextual service
export class CloudflareEnvironment extends Context.Service<
CloudflareEnvironment,
CloudflareEnv
>()("app/CloudflareEnvironment") {}
// Safe runner bounded to the active request
export function runWithCloudflareEnv<A, E>(
env: CloudflareEnv,
program: Effect.Effect<A, E, CloudflareEnvironment>,
): Promise<A> {
return Effect.runPromise(
Effect.provideService(program, CloudflareEnvironment, env),
);
}

Clean Endpoint Consumption

In the Better Auth API catch-all endpoint (src/routes/api/auth/[...all]/+server.ts), the request executes as an Effect with explicit error handling:

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);
};

This pattern provides:

  1. Total Isolation: Zero shared state between concurrent requests.
  2. Explicit Typings: Environment requirements are codified into Effect type signatures.
  3. Resilience: Expected failures are handled without unhandled isolate crashes.

4. Distributed Persistence: Cloudflare D1 & Declarative Migrations

Cloudflare D1 provides a serverless relational database built on SQLite, natively distributed across Cloudflare’s global edge.

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

A. Why Edge SQLite?

When code executes within milliseconds of the end user, querying an origin database located across the world reintroduces 100 to 200 ms of network latency. D1 executes queries locally on the edge node, eliminating latency bottlenecks.

B. Declarative Migrations

Schema changes are stored in the migrations/ directory as numbered SQL files (e.g., 0001_better_auth.sql).

The workflow is predictable:

Terminal window
# Apply to local development SQLite database
bunx wrangler d1 migrations apply template-cloudflare-svelte --local
# Apply to production remote D1 database
bunx wrangler d1 migrations apply template-cloudflare-svelte --remote

5. Edge Authentication: Better Auth & Session Propagation

Authentication is frequently challenging in serverless setups. This architecture uses Better Auth with its better-auth-cloudflare adapter.

A. Native D1 Driver

Instead of bundling heavy ORMs, Better Auth connects directly to the D1 binding using 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. Session Propagation in SvelteKit (hooks.server.ts)

To make user sessions accessible across server routes (+page.server.ts, +layout.server.ts), the hook extracts and attaches the session into event.locals:

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);

And in src/app.d.ts:

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

C. Isomorphic Client Helper

On the frontend, src/lib/auth-client.ts exports the client:

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

It handles login, signup, and signout reactively while managing encrypted cookie headers automatically.


6. Internationalization, Styling & Design System

A. Paraglide JS: Compile-Time i18n

Instead of runtime JSON dictionaries, Paraglide JS compiles messages into typed TypeScript functions:

  • Zero runtime JSON parsing overhead.
  • Autocompletion and type-checked parameters (m.app_title()).
  • HTML language and text direction attributes injected dynamically via transformPageChunk.
  • Complete SEO hreflang headers and a dynamic /sitemap.xml endpoint.

B. Tailwind CSS 4, Oxide Engine & Animations

The template uses Tailwind CSS 4 powered by its Rust-based Oxide engine:

  • Direct configuration in CSS (@import "tailwindcss"; @plugin "@tailwindcss/forms";).
  • Fast Vite bundling without heavy JavaScript configuration files.
  • Smooth UI motion through @plugin "tailwindcss-animate" imported directly in src/routes/layout.css.
  • Automated class sorting using Oxfmt.

C. On-Demand shadcn-svelte Primitives (bits-ui) with Zero Boilerplate Bloat

Starter templates often clutter projects with dozens of pre-generated UI components that go unused.

This template adopts an intentional, lean alternative: on-demand UI primitives with zero boilerplate noise:

  • Native components.json Configuration: Fully configured for the shadcn-svelte CLI, built on top of accessible, unstyled bits-ui primitives.
  • Shared cn() Utility: src/lib/utils.ts configures class merging with clsx and tailwind-merge.
  • Zero Dead Code: No unused components are committed into src/lib/components/ui/ by default. Developers or AI agents generate only what is strictly needed.
  • Structure Auditing: Oxlint is configured with @shadcn/lint to ensure all newly introduced UI components adhere to clean conventions.

D. Neutral Design System (DESIGN.md) and UI Craft & Motion Rules (AGENTS.md)

For a template to be instantly customized by developers or coding agents, visual identity must be cleanly documented:

  • Token Specification: DESIGN.md and .impeccable/design.json define semantic color layers (surfaces, typography, borders, accents), spacing scales, and border radiuses.
  • Craft & Motion Directives: AGENTS.md enforces interface polish standards:
    • Purposeful Motion: Every animation must serve an interaction purpose and remain interruptible.
    • 60fps Compositor-Friendly Transitions: Focusing on GPU-accelerated transform/opacity changes.
    • Accessibility: Full keyboard navigation, strong contrast ratios, and strict support for prefers-reduced-motion.

E. AI-Assisted Setup Protocol (SETUP.md)

AI coding agents are now integral to modern scaffolding workflows. SETUP.md defines a structured, interactive onboarding protocol:

When tasked with bootstrapping a new project from this template, the agent initiates an interactive 4-step checklist:

  1. Project Identity: App name, short description, target production URL.
  2. Design System & Palette: Dark minimalist theme or brand-specific palette tokens.
  3. i18n: Primary locale and supported language tags in Paraglide.
  4. Auth & D1: Enabled Better Auth providers (Email/Password, Social OAuth) and D1 database identifiers.

The agent then applies all configuration updates across the codebase in a single cohesive turn, delivering a fully personalized production app in seconds.


7. Typing Challenges and CI/CD Pipeline

Building this architecture highlighted a classic Cloudflare Workers challenge: the circular dependency between build artifacts and type generation.

Eliminating worker-configuration.d.ts from Version Control

Initially, the definitions file worker-configuration.d.ts generated by wrangler types (exceeding 16,000 lines) was committed to Git. This introduced friction:

  • Git History Bloat: Every local type regeneration or config change rewrote thousands of generated lines.
  • The Entrypoint Paradox: In wrangler.jsonc, the main entrypoint is declared as "main": ".svelte-kit/cloudflare/_worker.js". On a clean clone (notably in CI), this compiled file does not yet exist. Running wrangler types --check before building failed because mainModule was missing.

The template eliminates this issue at the root by removing worker-configuration.d.ts from Git tracking (added to .gitignore).

Decoupling Type Generation (Local & CI)

Type generation is now cleanly split across two distinct phases:

  1. In local development: The dev script checks types and starts Vite:

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

    The bun run gen command remains available to explicitly refresh TypeScript definitions on demand.

  2. In the GitHub Actions CI pipeline: The build compiles the SvelteKit app first via Vite, then generates types directly on the build runner:

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

In package.json, the build script is clean and deterministic:

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

Complete Automated Quality Suite

The entire verification pipeline runs in under two minutes on GitHub Actions:

  • bun run lint: Oxlint for fast static analysis, augmented by @shadcn/lint to audit component structure.
  • bun run format:check: Oxfmt enforcing code and markdown formatting.
  • bun run build & bun run check: SvelteKit and Wrangler type validation.
  • bun run test:unit: Vitest unit tests verifying Effect environment isolation.
  • bun run test:e2e: Playwright end-to-end tests validating the app served by the local Worker (wrangler dev), with explicit assertions on homepage features.

8. Conclusion

This SvelteKit 5 + Cloudflare Workers architecture bridges the gap between server management complexity and high-performance requirements.

By combining:

  • SvelteKit 5 runes reactivity,
  • Cloudflare Workers V8 isolate speed,
  • Cloudflare D1 distributed SQLite persistence,
  • Effect robust environment encapsulation,
  • And a modern toolchain (Bun, Oxlint, Better Auth),

it provides an ultra-fast, robust foundation ready for production.

The complete template source code is available on GitHub: 👉 FRFlo/template-cloudflare-svelte