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