Cloudflare
function create_cloudflare_backend(config: CloudflareBackendConfig): BackendThe 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}| Option | Type | Description |
|---|---|---|
d1 | D1Database | D1 database binding from env |
r2 | R2Bucket | R2 bucket binding from env |
on_event | EventHandler | Optional callback for storage events |
Setup Guide
1. Create D1 Database
# Create the databasewrangler d1 create corpus-db
# Note the database_id from the output2. Create R2 Bucket
wrangler r2 bucket create corpus-bucket3. Run Database Migration
Before first use, create the required table:
import { CORPUS_MIGRATION_SQL } from '@f0rbit/corpus'
// In a setup script or migration workerawait env.CORPUS_DB.exec(CORPUS_MIGRATION_SQL)Or create a migration file and run with Wrangler:
-- migrations/0001_init.sqlCREATE TABLE IF NOT EXISTS corpus_snapshots ( store_id TEXT NOT NULL, version TEXT NOT NULL, parents TEXT NOT NULL, created_at TEXT NOT NULL, invoked_at TEXT, content_hash TEXT NOT NULL, content_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, data_key TEXT NOT NULL, tags TEXT, PRIMARY KEY (store_id, version));
CREATE INDEX IF NOT EXISTS idx_store_created ON corpus_snapshots(store_id, created_at);CREATE INDEX IF NOT EXISTS idx_content_hash ON corpus_snapshots(store_id, content_hash);wrangler d1 migrations apply corpus-db4. 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
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
| Scenario | Recommended |
|---|---|
| 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
- Batch reads: Use
list()to get metadata, then fetch only what you need - Deduplication: Corpus automatically deduplicates content, reducing R2 storage
- Caching: Use Layered backend with Memory for hot data
- Indexes: D1 indexes are created by the migration for common queries
See Also
- Cloudflare Deployment Guide - Complete setup walkthrough
- Memory - For local testing
- Layered - Add caching layer