Tools: SvelteKit to Cloudflare Workers for Free Deploying

Tools: SvelteKit to Cloudflare Workers for Free Deploying

What you get for free

Prerequisites

Step 1: Install the adapter

Step 2: Configure svelte.config.js

Step 3: Create wrangler.jsonc

Step 4: Build and deploy

Step 5: Connect a custom domain (optional)

Common gotchas

Node.js compatibility

Environment variables

Worker size limits

Testing locally

GitHub Actions for CI/CD

Why not Cloudflare Pages?

Monitoring your usage

Why I use this setup For years, deploying a full-stack web app meant either paying for a VPS or wrestling with complex container orchestration. Cloudflare Workers changes that. You can run SvelteKit with server-side rendering, API routes, and edge caching, all without spending a dime on the generous free tier. This guide walks through the exact setup I use for this site. No theoretical fluff, just working configuration. Cloudflare's free tier is surprisingly capable: Compare this to Vercel's hobby tier (limited to 10s serverless function duration) or Netlify's build minute limits. Workers' V8 isolate model means faster cold starts and more predictable pricing if you ever need to scale. If you don't have a SvelteKit project yet: Cloudflare Workers runs JavaScript in V8 isolates, not Node.js. SvelteKit needs an adapter to bridge this gap: The adapter-cloudflare package handles both Workers and Pages deployment. For new Workers Static Assets (the modern approach), this is the adapter you want, not the older adapter-cloudflare-workers. Replace the default adapter in your svelte.config.js: Key options explained: Create a wrangler.jsonc file in your project root: What each field does: Build your app locally first to verify everything works: This creates a .svelte-kit/cloudflare/ directory containing your optimized app. Now install Wrangler CLI and deploy: After the first deploy, Cloudflare gives you a *.workers.dev subdomain. Free hosting, no domain required. If you have a domain managed by Cloudflare: If your DNS is elsewhere, add a CNAME record pointing to your *.workers.dev subdomain. Not all pnpm packages work in Workers. Anything relying on fs, native bindings, or certain Node.js internals will fail. Check the Cloudflare Workers runtime API docs before adding heavy dependencies. If you hit issues, try adding the nodejs_compat flag (shown in the config above). This enables polyfills for common Node.js modules. Don't use process.env in Workers. Instead, use SvelteKit's built-in $env modules: For Cloudflare-specific bindings (KV, Durable Objects), access them via the platform object: The final Worker bundle must stay under Cloudflare's size limits (currently around 1MB gzipped). If your build fails with a size error: You can test the production build locally with Wrangler: This runs the exact same code that deploys to production, including platform bindings if you've configured them in wrangler.jsonc. For automatic deployments on push: Create a Cloudflare API token with "Cloudflare Workers" edit permissions and add it as CLOUDFLARE_API_TOKEN in your repository secrets. Both Workers and Pages can host SvelteKit, but there are differences: I prefer Workers because the adapter-cloudflare generates a single _worker.js file that handles routing, SSR, and static assets. It's simpler mentally. One entry point, one mental model. The Cloudflare dashboard shows your request volume and CPU time. Keep an eye on: If you're approaching limits, consider: Deploying SvelteKit to Cloudflare Workers gives you a production-grade hosting stack at zero cost. The edge runtime means faster response times for global visitors, and the free tier is genuinely usable, not just a teaser to get you hooked. The setup is minimal: install an adapter, add a config file, run two commands. The rest is just SvelteKit doing what it does best. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

pnpx sv create my-app cd my-app pnpm install pnpx sv create my-app cd my-app pnpm install pnpx sv create my-app cd my-app pnpm install pnpm add -D @sveltejs/adapter-cloudflare pnpm add -D @sveltejs/adapter-cloudflare pnpm add -D @sveltejs/adapter-cloudflare import adapter from '@sveltejs/adapter-cloudflare'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ // See below for options config: undefined, platformProxy: { configPath: undefined, environment: undefined, persist: undefined }, fallback: 'plaintext', routes: { include: ['/*'], exclude: ['<files>', '<build>', '<redirects>'] } }) } }; export default config; import adapter from '@sveltejs/adapter-cloudflare'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ // See below for options config: undefined, platformProxy: { configPath: undefined, environment: undefined, persist: undefined }, fallback: 'plaintext', routes: { include: ['/*'], exclude: ['<files>', '<build>', '<redirects>'] } }) } }; export default config; import adapter from '@sveltejs/adapter-cloudflare'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ // See below for options config: undefined, platformProxy: { configPath: undefined, environment: undefined, persist: undefined }, fallback: 'plaintext', routes: { include: ['/*'], exclude: ['<files>', '<build>', '<redirects>'] } }) } }; export default config; { "name": "my-sveltekit-app", "main": ".svelte-kit/cloudflare/_worker.js", "compatibility_flags": ["nodejs_als", "nodejs_compat"], "compatibility_date": "2024-09-23", "assets": { "binding": "ASSETS", "directory": ".svelte-kit/cloudflare" }, "routes": [ { "pattern": "yourdomain.com", "custom_domain": true } ] } { "name": "my-sveltekit-app", "main": ".svelte-kit/cloudflare/_worker.js", "compatibility_flags": ["nodejs_als", "nodejs_compat"], "compatibility_date": "2024-09-23", "assets": { "binding": "ASSETS", "directory": ".svelte-kit/cloudflare" }, "routes": [ { "pattern": "yourdomain.com", "custom_domain": true } ] } { "name": "my-sveltekit-app", "main": ".svelte-kit/cloudflare/_worker.js", "compatibility_flags": ["nodejs_als", "nodejs_compat"], "compatibility_date": "2024-09-23", "assets": { "binding": "ASSETS", "directory": ".svelte-kit/cloudflare" }, "routes": [ { "pattern": "yourdomain.com", "custom_domain": true } ] } pnpm add -g wrangler wrangler login wrangler deploy pnpm add -g wrangler wrangler login wrangler deploy pnpm add -g wrangler wrangler login wrangler deploy // +page.server.js import { SECRET_API_KEY } from '$env/static/private'; export async function load() { // Use SECRET_API_KEY here } // +page.server.js import { SECRET_API_KEY } from '$env/static/private'; export async function load() { // Use SECRET_API_KEY here } // +page.server.js import { SECRET_API_KEY } from '$env/static/private'; export async function load() { // Use SECRET_API_KEY here } // hooks.js or +server.js export async function handle({ event, resolve }) { const { env } = event.platform; // env.MY_KV_NAMESPACE, env.MY_DURABLE_OBJECT, etc. return resolve(event); } // hooks.js or +server.js export async function handle({ event, resolve }) { const { env } = event.platform; // env.MY_KV_NAMESPACE, env.MY_DURABLE_OBJECT, etc. return resolve(event); } // hooks.js or +server.js export async function handle({ event, resolve }) { const { env } = event.platform; // env.MY_KV_NAMESPACE, env.MY_DURABLE_OBJECT, etc. return resolve(event); } pnpm build wrangler dev .svelte-kit/cloudflare/_worker.js pnpm build wrangler dev .svelte-kit/cloudflare/_worker.js pnpm build wrangler dev .svelte-kit/cloudflare/_worker.js # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: pnpm install - run: pnpm build - run: pnpx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: pnpm install - run: pnpm build - run: pnpx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: pnpm install - run: pnpm build - run: pnpx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - 100,000 requests/day — enough for most personal projects and small sites - 10ms CPU time per request — SvelteKit runs comfortably within this - 1 GB of KV storage — for simple config or session data - Custom domains — connect your own domain at no extra cost - Node.js 18+ installed - A Cloudflare account (free tier works fine) - A SvelteKit project ready to deploy - config: Path to your Wrangler config file (we'll create this next) - platformProxy: Controls how local bindings are emulated during development - fallback: 'plaintext' gives you a simple 404 page; use 'spa' if you need client-side routing for unmatched paths - routes.exclude: Tells Cloudflare which requests can bypass the Worker and serve static assets directly. This saves you invocation costs. - name: Your Worker's identifier in the Cloudflare dashboard - main: The entry point SvelteKit generates. Don't change this. - compatibility_flags: nodejs_als is required for SvelteKit's async context; nodejs_compat helps with NPM packages that use Node.js APIs - compatibility_date: Cloudflare's runtime version. Bump this periodically for new features. - assets: Tells Workers where your static files live - routes: Connects custom domains (you can add this later if you don't have a domain yet) - Go to Workers & Pages in your Cloudflare dashboard - Select your Worker - Click "Triggers" → "Add Custom Domain" - Enter your domain - Check for large dependencies bundled on the server side - Move heavy libraries to client-only imports if possible - Use dynamic imports for code splitting - Requests: Free tier = 100k/day. At ~10k requests/day, you've got a 10-day buffer. - CPU time: SvelteKit usually runs under 5ms per request unless you're doing heavy computation. - Prerendering more pages at build time - Caching API responses at the edge - Using KV for frequently-read, rarely-written data