Skip to content

Cloudflare

function create_cloudflare_backend(config: CloudflareBackendConfig): Backend

The Cloudflare backend provides production-ready, globally distributed storage using Cloudflare’s infrastructure. It uses D1 (SQLite at the edge) for metadata queries and R2 (S3-compatible object storage) for binary data.

Why Use Cloudflare Backend?

  • Global distribution: Data replicated across Cloudflare’s network
  • Edge performance: Low latency from any location
  • Serverless: No servers to manage, scales automatically
  • Cost effective: Pay only for what you use
  • Durable: Enterprise-grade storage reliability

Basic Usage

import {
create_corpus,
create_cloudflare_backend,
define_store,
json_codec
} from '@f0rbit/corpus/cloudflare'
import { z } from 'zod'
const SessionSchema = z.object({
userId: z.string(),
data: z.record(z.unknown()),
expiresAt: z.string(),
})
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const backend = create_cloudflare_backend({
d1: env.CORPUS_DB,
r2: env.CORPUS_BUCKET,
})
const corpus = create_corpus()
.with_backend(backend)
.with_store(define_store('sessions', json_codec(SessionSchema)))
.build()
// Handle request using corpus...
return new Response('OK')
}
}

Configuration

type CloudflareBackendConfig = {
d1: D1Database
r2: R2Bucket
on_event?: EventHandler
}
OptionTypeDescription
d1D1DatabaseD1 database binding from env
r2R2BucketR2 bucket binding from env
on_eventEventHandlerOptional callback for storage events

Setup Guide

1. Create D1 Database

Terminal window
# Create the database
wrangler d1 create corpus-db
# Note the database_id from the output

2. Create R2 Bucket

Terminal window
wrangler r2 bucket create corpus-bucket

3. Set Up Database Migrations

Corpus uses Drizzle ORM for schema management. Install Drizzle Kit and generate migrations from the exported schemas:

Terminal window
bun add -D drizzle-kit

Create drizzle.config.ts:

import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'sqlite',
driver: 'd1-http',
schema: [
'./node_modules/@f0rbit/corpus/schema.js',
'./node_modules/@f0rbit/corpus/observations/schema.js',
],
out: './migrations',
})

Generate and apply migrations:

Terminal window
# Generate migration files from corpus schemas
bunx drizzle-kit generate
# Apply migrations to your D1 database
wrangler d1 migrations apply corpus-db

This creates the corpus_snapshots and corpus_observations tables with all required indexes. See the Schema API for full schema details.

4. Configure wrangler.toml

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "CORPUS_DB"
database_name = "corpus-db"
database_id = "your-database-id-here"
[[r2_buckets]]
binding = "CORPUS_BUCKET"
bucket_name = "corpus-bucket"

5. TypeScript Types

src/env.d.ts
interface Env {
CORPUS_DB: D1Database
CORPUS_BUCKET: R2Bucket
}

Complete Worker Example

import {
create_corpus,
create_cloudflare_backend,
define_store,
json_codec
} from '@f0rbit/corpus/cloudflare'
import { z } from 'zod'
const NoteSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
createdAt: z.string(),
})
type Note = z.infer<typeof NoteSchema>
interface Env {
CORPUS_DB: D1Database
CORPUS_BUCKET: R2Bucket
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const corpus = create_corpus()
.with_backend(create_cloudflare_backend({
d1: env.CORPUS_DB,
r2: env.CORPUS_BUCKET,
}))
.with_store(define_store('notes', json_codec(NoteSchema)))
.build()
const url = new URL(request.url)
// GET /notes - List all notes
if (url.pathname === '/notes' && request.method === 'GET') {
const notes: Note[] = []
for await (const meta of corpus.stores.notes.list({ limit: 100 })) {
const result = await corpus.stores.notes.get(meta.version)
if (result.ok) notes.push(result.value.data)
}
return Response.json(notes)
}
// POST /notes - Create a note
if (url.pathname === '/notes' && request.method === 'POST') {
const body = await request.json() as Omit<Note, 'id' | 'createdAt'>
const note: Note = {
...body,
id: crypto.randomUUID(),
createdAt: new Date().toISOString(),
}
const result = await corpus.stores.notes.put(note)
if (result.ok) {
return Response.json({
id: note.id,
version: result.value.version
}, { status: 201 })
}
return Response.json({ error: result.error.kind }, { status: 500 })
}
return new Response('Not Found', { status: 404 })
}
}

Import Path

Error Handling

Handle Cloudflare-specific errors gracefully:

const result = await corpus.stores.data.put(item)
if (!result.ok) {
switch (result.error.kind) {
case 'storage_error':
// D1 or R2 operation failed
console.error(`${result.error.operation} failed:`, result.error.cause)
return new Response('Storage error', { status: 503 })
case 'encode_error':
return new Response('Invalid data format', { status: 400 })
default:
return new Response('Internal error', { status: 500 })
}
}

When to Use

ScenarioRecommended
Cloudflare Workers✅ Yes
Production workloads✅ Yes
Global applications✅ Yes
High availability needs✅ Yes
Local development⚠️ Use Memory for tests
Non-Cloudflare hosting❌ No

Performance Tips

  1. Batch reads: Use list() to get metadata, then fetch only what you need
  2. Deduplication: Corpus automatically deduplicates content, reducing R2 storage
  3. Caching: Use Layered backend with Memory for hot data
  4. Indexes: D1 indexes are created by the migration for common queries

See Also