sveltekit-cloudflare-worker

Full Workers platform from SvelteKit

Export Durable Objects, define scheduled handlers, and intercept requests before SvelteKit — all from a single src/worker.ts file.

How it works

Your fetch handler runs first. Return a Response to handle it, or return nothing to let SvelteKit take over. Class exports like Durable Objects and Workflows are re-exported from the final worker automatically.

Fetch interception

GET /api/hello — handled by the worker before SvelteKit sees it.

Durable Object — Counter

Persistent counter using DurableObject with SQLite storage. State survives restarts.

Durable Object — WebSocket Chat

Real-time chat backed by a ChatRoom Durable Object with WebSocket hibernation. Rate limited by a native RateLimit binding.

Cloudflare Workflow

A WorkflowEntrypoint with 3 steps, reporting progress to a DurableObject via RPC, which broadcasts updates over WebSocket.

SvelteKit fallthrough

Requests that don't match the worker's fetch handler fall through to SvelteKit. This page itself is served by SvelteKit.

Code examples

Works with Hono

Mount a full Hono app for your API. Unmatched routes fall through to SvelteKit.

import type { WorkerFetch } from 'sveltekit-cloudflare-worker';
import { Hono } from 'hono';

const api = new Hono<{ Bindings: Env }>()
  .basePath('/api')
  .get('/users', async (c) => {
    const users = await c.env.DB.prepare('SELECT * FROM users').all();
    return c.json(users.results);
  })
  .post('/users', async (c) => {
    const body = await c.req.json();
    await c.env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind(body.name).run();
    return c.json({ ok: true }, 201);
  });

export const fetch: WorkerFetch<Env> = async (req, env, ctx, next) => {
  const res = await api.fetch(req, env, ctx);
  if (res.status === 404) return; // fall through to SvelteKit
  return res;
};

Transform SvelteKit responses

Call next() to get SvelteKit's response, then modify it.

export const fetch: WorkerFetch = async (req, env, ctx, next) => {
  const url = new URL(req.url);

  if (url.pathname.startsWith('/api/')) {
    return new Response('handled by worker');
  }

  // Add security headers to all SvelteKit responses
  const res = await next();
  const headers = new Headers(res.headers);
  headers.set('X-Frame-Options', 'DENY');
  headers.set('X-Content-Type-Options', 'nosniff');
  return new Response(res.body, { status: res.status, headers });
};

Durable Objects + WebSocket + Rate Limiting

The code powering this demo page.

import type { WorkerFetch } from 'sveltekit-cloudflare-worker';
import { DurableObject } from 'cloudflare:workers';

export const fetch: WorkerFetch<Env> = async (req, env, ctx, next) => {
  const url = new URL(req.url);

  if (url.pathname === '/api/do/increment') {
    const stub = env.MY_DO.get(env.MY_DO.idFromName('demo'));
    return Response.json({ count: await stub.increment() });
  }

  if (url.pathname === '/api/chat') {
    const roomId = env.CHAT_ROOM.idFromName('demo');
    return env.CHAT_ROOM.get(roomId).fetch(req);
  }
};

export class MyDurableObject extends DurableObject {
  async increment() {
    const count = ((await this.ctx.storage.get('count')) ?? 0) + 1;
    await this.ctx.storage.put('count', count);
    return count;
  }
}

export class ChatRoom extends DurableObject {
  async fetch(request) {
    const pair = new WebSocketPair();
    this.ctx.acceptWebSocket(pair[1]);
    return new Response(null, { status: 101, webSocket: pair[0] });
  }

  async webSocketMessage(ws, message) {
    const { success } = await this.env.RATELIMIT_CHAT.limit({ key: name });
    if (!success) return ws.send(JSON.stringify({ type: 'system', text: 'Rate limited' }));
    this.ctx.getWebSockets().forEach(s => s.send(message));
  }
}
sveltekit-cloudflare-worker