Claude Code for Cloudflare Workers: Edge Computing and D1 Database — Claude Skills 360 Blog
Blog / Development / Claude Code for Cloudflare Workers: Edge Computing and D1 Database
Development

Claude Code for Cloudflare Workers: Edge Computing and D1 Database

Published: July 8, 2026
Read time: 9 min read
By: Claude Skills 360

Cloudflare Workers runs JavaScript at the edge — in data centers close to users worldwide, not in a central region. For the right workloads (API routing, authentication, caching, data transformation), Workers eliminate the cold start latency of traditional serverless and give sub-millisecond response times. Claude Code understands the Workers runtime constraints and generates code that works within the 50ms CPU time limit and the Web API-based environment.

This guide covers Cloudflare Workers with Claude Code: routing with Hono, KV for caching, D1 for relational data, R2 for files, and Durable Objects for stateful edge.

Workers Setup

CLAUDE.md for Cloudflare Workers Projects

## Cloudflare Workers Project

- Runtime: Workers (not Node.js) — uses Web APIs (fetch, Response, Request, crypto.subtle)
- Framework: Hono (lightweight, fast, designed for edge)
- Database: Cloudflare D1 (SQLite) via Drizzle ORM
- Cache: Cloudflare KV for session data and cache
- Storage: R2 for user-uploaded files
- Local dev: `wrangler dev` (runs Workers runtime locally)

## What's NOT available in Workers
- No Node.js built-ins (no fs, no path, no os, no child_process)
- No Buffer — use Uint8Array and TextEncoder/TextDecoder
- No import of Node.js modules unless they have a browser-compatible build
- Limited CPU time: 50ms per request (exceptions: Durable Objects, paid plan 30s)

## Wrangler conventions
- wrangler.toml: declares KV namespaces, D1 databases, R2 buckets, bindings
- Env type: Bindings are in the `Env` interface, accessed via `c.env.KV_NAMESPACE`
- TypeScript: generate types with `wrangler types`
- Testing: `wrangler dev --local` for local dev, vitest for unit tests

Hono Application Structure

// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { bearerAuth } from 'hono/bearer-auth';
import { drizzle } from 'drizzle-orm/d1';
import { usersRouter } from './routes/users';
import { productsRouter } from './routes/products';
import type { Env } from './types';

const app = new Hono<{ Bindings: Env }>();

// Middleware
app.use('*', logger());
app.use('*', cors({
  origin: (origin) => origin, // Echo origin (allows any — configure for production)
  credentials: true,
}));

// Protected routes
app.use('/api/*', bearerAuth({ token: '' })); // Custom token verification below

// Custom JWT verification middleware
app.use('/api/*', async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '');
  if (!token) return c.json({ error: 'Unauthorized' }, 401);
  
  // Web Crypto API — available in Workers
  const payload = await verifyJWT(token, c.env.JWT_SECRET);
  if (!payload) return c.json({ error: 'Invalid token' }, 401);
  
  c.set('userId', payload.sub);
  await next();
});

app.route('/api/users', usersRouter);
app.route('/api/products', productsRouter);

app.get('/health', (c) => c.json({ ok: true, region: c.req.header('CF-Ray') }));

export default app;
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-10-01"
node_compat = false  # We don't need Node compatibility

[[kv_namespaces]]
binding = "SESSIONS"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-id"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

[[r2_buckets]]
binding = "FILES"
bucket_name = "my-files"

[vars]
ENVIRONMENT = "production"

[secrets]
# Set with: wrangler secret put JWT_SECRET
JWT_SECRET = ""

D1 Database with Drizzle

Build the users table and CRUD API using D1.
// src/db/schema.ts
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  passwordHash: text('password_hash').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .$defaultFn(() => new Date()),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
    .$defaultFn(() => new Date()),
}, (table) => ({
  emailIdx: index('users_email_idx').on(table.email),
}));
// src/routes/users.ts
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/d1';
import { users } from '../db/schema';
import { eq } from 'drizzle-orm';
import { hashPassword, verifyPassword } from '../lib/crypto';
import type { Env } from '../types';

const router = new Hono<{ Bindings: Env; Variables: { userId: string } }>();

router.get('/:id', async (c) => {
  const db = drizzle(c.env.DB, { schema: { users } });
  const userId = c.req.param('id');
  
  // D1 allows only current user to see their profile
  if (c.get('userId') !== userId) {
    return c.json({ error: 'Forbidden' }, 403);
  }
  
  const [user] = await db.select({
    id: users.id,
    email: users.email,
    name: users.name,
    createdAt: users.createdAt,
  }).from(users).where(eq(users.id, userId));
  
  if (!user) return c.json({ error: 'Not found' }, 404);
  return c.json({ user });
});

router.post('/', async (c) => {
  const db = drizzle(c.env.DB, { schema: { users } });
  const body = await c.req.json();
  
  // Workers has Web Crypto for hashing — no bcrypt (it's a Node.js package)
  const passwordHash = await hashPassword(body.password);
  
  try {
    const [user] = await db.insert(users)
      .values({ email: body.email, name: body.name, passwordHash })
      .returning({ id: users.id, email: users.email, name: users.name });
    
    return c.json({ user }, 201);
  } catch (error: any) {
    if (error.message?.includes('UNIQUE constraint')) {
      return c.json({ error: 'Email already exists' }, 409);
    }
    throw error;
  }
});

export { router as usersRouter };

KV for Caching and Sessions

Cache expensive API responses in KV.
Sessions in KV — not JWT — for instant revocation.
// src/lib/cache.ts
export class KVCache {
  constructor(private readonly kv: KVNamespace) {}
  
  async get<T>(key: string): Promise<T | null> {
    const value = await this.kv.get(key, 'json');
    return value as T | null;
  }
  
  async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
    await this.kv.put(key, JSON.stringify(value), { expirationTtl: ttlSeconds });
  }
  
  async del(key: string): Promise<void> {
    await this.kv.delete(key);
  }
}

// Session management
export async function createSession(
  kv: KVNamespace,
  userId: string,
): Promise<string> {
  const sessionId = crypto.randomUUID();
  await kv.put(
    `session:${sessionId}`,
    JSON.stringify({ userId, createdAt: Date.now() }),
    { expirationTtl: 7 * 24 * 60 * 60 } // 7 days
  );
  return sessionId;
}

export async function getSession(
  kv: KVNamespace,
  sessionId: string,
): Promise<{ userId: string } | null> {
  return kv.get(`session:${sessionId}`, 'json');
}

export async function deleteSession(kv: KVNamespace, sessionId: string): Promise<void> {
  await kv.delete(`session:${sessionId}`);
}

R2 File Storage

Handle file uploads — store in R2, generate signed download URLs.
// src/routes/files.ts
router.post('/upload', async (c) => {
  const userId = c.get('userId');
  const formData = await c.req.formData();
  const file = formData.get('file') as File;
  
  if (!file) return c.json({ error: 'No file provided' }, 400);
  
  // Validate file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!allowedTypes.includes(file.type)) {
    return c.json({ error: 'File type not allowed' }, 400);
  }
  
  // Validate size (10MB max)
  if (file.size > 10 * 1024 * 1024) {
    return c.json({ error: 'File too large (max 10MB)' }, 400);
  }
  
  const key = `${userId}/${crypto.randomUUID()}-${file.name}`;
  
  await c.env.FILES.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type,
    },
    customMetadata: {
      userId,
      originalName: file.name,
      uploadedAt: new Date().toISOString(),
    },
  });
  
  // Return the key — client uses a separate endpoint to get a signed URL
  return c.json({ key }, 201);
});

router.get('/download/:key{.+}', async (c) => {
  const key = c.req.param('key');
  const userId = c.get('userId');
  
  const object = await c.env.FILES.get(key);
  
  if (!object) return c.json({ error: 'Not found' }, 404);
  
  // Verify ownership
  if (object.customMetadata?.userId !== userId) {
    return c.json({ error: 'Forbidden' }, 403);
  }
  
  return new Response(object.body, {
    headers: {
      'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
      'Content-Disposition': `attachment; filename="${object.customMetadata?.originalName}"`,
    },
  });
});

For deploying SvelteKit and Astro to Cloudflare Pages (not Workers), see the SvelteKit guide and Astro guide. For Durable Objects — the stateful continuation of Workers — Claude Code generates the class-based pattern for collaborative features. The Claude Skills 360 bundle includes Cloudflare Workers skill sets for edge API patterns. Start with the free tier to generate a Workers API with D1 and KV.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free