Cloudflare Adapter
@trokky/adapter-cloudflare provides storage using Cloudflare D1 (SQLite database) and R2 (object storage), enabling edge-native CMS deployments.
Installation
Section titled “Installation”npm install @trokky/adapter-cloudflare// In Cloudflare Workerimport { CloudflareAdapter } from '@trokky/adapter-cloudflare';
export default { async fetch(request: Request, env: Env) { const storage = new CloudflareAdapter({ d1: env.DB, r2: env.STORAGE, });
// Use with TrokkyCloudflare const trokky = TrokkyCloudflare.create({ storage, // ... });
return trokky.handle(request); },};Configuration
Section titled “Configuration”| Option | Type | Required | Description |
|---|---|---|---|
d1 | D1Database | Yes | D1 database binding |
r2 | R2Bucket | Yes | R2 bucket binding |
mediaPrefix | string | No | Prefix for R2 media keys |
1. Create D1 Database
Section titled “1. Create D1 Database”wrangler d1 create trokky-db2. Create R2 Bucket
Section titled “2. Create R2 Bucket”wrangler r2 bucket create trokky-media3. Configure wrangler.toml
Section titled “3. Configure wrangler.toml”name = "trokky-cms"main = "src/index.ts"compatibility_date = "2024-01-01"
[[d1_databases]]binding = "DB"database_name = "trokky-db"database_id = "your-database-id"
[[r2_buckets]]binding = "STORAGE"bucket_name = "trokky-media"4. Run Migrations
Section titled “4. Run Migrations”Create migration file:
-- migrations/0001_init.sqlCREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, type TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS idx_documents_type ON documents(type);CREATE INDEX IF NOT EXISTS idx_documents_created ON documents(created_at);
CREATE TABLE IF NOT EXISTS media ( id TEXT PRIMARY KEY, filename TEXT NOT NULL, mime_type TEXT NOT NULL, size INTEGER NOT NULL, metadata TEXT, created_at TEXT NOT NULL);Apply migration:
wrangler d1 migrations apply trokky-dbComplete Worker Example
Section titled “Complete Worker Example”import { TrokkyCloudflare } from '@trokky/cloudflare';import { CloudflareAdapter } from '@trokky/adapter-cloudflare';
interface Env { DB: D1Database; STORAGE: R2Bucket; JWT_SECRET: string; ADMIN_USERNAME: string; ADMIN_PASSWORD: string;}
const schemas = [ { name: 'post', title: 'Post', fields: [ { name: 'title', type: 'string', required: true }, { name: 'content', type: 'richtext' }, ], },];
export default { async fetch(request: Request, env: Env): Promise<Response> { const storage = new CloudflareAdapter({ d1: env.DB, r2: env.STORAGE, });
const trokky = TrokkyCloudflare.create({ schemas, storage, security: { jwtSecret: env.JWT_SECRET, adminUser: { username: env.ADMIN_USERNAME, password: env.ADMIN_PASSWORD, }, }, });
return trokky.handle(request); },};API Reference
Section titled “API Reference”Constructor
Section titled “Constructor”new CloudflareAdapter(options: CloudflareAdapterOptions)Methods
Section titled “Methods”All standard StorageAdapter methods are implemented:
// DocumentscreateDocument(collection: string, data: any): Promise<Document>getDocument(collection: string, id: string): Promise<Document | null>updateDocument(collection: string, id: string, data: any): Promise<Document>deleteDocument(collection: string, id: string): Promise<void>listDocuments(collection: string, options?: ListOptions): Promise<Document[]>
// MediauploadMedia(file: ArrayBuffer, metadata: MediaMetadata): Promise<MediaAsset>getMedia(id: string): Promise<MediaAsset | null>deleteMedia(id: string): Promise<void>listMedia(options?: ListOptions): Promise<MediaAsset[]>Media Handling
Section titled “Media Handling”Upload
Section titled “Upload”Media files are stored in R2:
const asset = await storage.uploadMedia(arrayBuffer, { filename: 'photo.jpg', mimeType: 'image/jpeg', alt: 'Photo description',});
// Returns{ _id: 'media-abc123', filename: 'photo.jpg', url: '/media/photo-abc123.jpg', // ...}Public URL
Section titled “Public URL”Configure custom domain for R2:
const storage = new CloudflareAdapter({ d1: env.DB, r2: env.STORAGE, publicUrl: 'https://media.example.com',});R2 Public Access
Section titled “R2 Public Access”Enable public access for your R2 bucket:
- Go to R2 > Your Bucket > Settings
- Enable “Public Access”
- Configure custom domain (optional)
D1 Schema
Section titled “D1 Schema”Documents Table
Section titled “Documents Table”CREATE TABLE documents ( id TEXT PRIMARY KEY, type TEXT NOT NULL, data TEXT NOT NULL, -- JSON string created_at TEXT NOT NULL, -- ISO 8601 updated_at TEXT NOT NULL -- ISO 8601);Media Table
Section titled “Media Table”CREATE TABLE media ( id TEXT PRIMARY KEY, filename TEXT NOT NULL, original_filename TEXT, mime_type TEXT NOT NULL, size INTEGER NOT NULL, width INTEGER, height INTEGER, alt TEXT, metadata TEXT, -- JSON string r2_key TEXT NOT NULL, -- R2 object key created_at TEXT NOT NULL);Users Table
Section titled “Users Table”CREATE TABLE users ( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL, name TEXT, created_at TEXT NOT NULL, last_login TEXT);Performance
Section titled “Performance”D1 Considerations
Section titled “D1 Considerations”- D1 is SQLite-based with read replicas at the edge
- Writes go to a primary location
- Reads are fast from edge replicas
- Use indexes for frequently queried fields
R2 Considerations
Section titled “R2 Considerations”- R2 provides global object storage
- No egress fees
- Use R2 public access or custom domain for media URLs
- Consider CDN caching for frequently accessed media
Optimization Tips
Section titled “Optimization Tips”- Index frequently filtered fields in D1
- Use pagination for large result sets
- Cache schema queries at the edge
- Enable R2 caching headers
Local Development
Section titled “Local Development”Wrangler Dev
Section titled “Wrangler Dev”wrangler devThis starts a local server with:
- Local D1 database
- Local R2 bucket simulation
Local D1 Data
Section titled “Local D1 Data”# Execute SQL locallywrangler d1 execute trokky-db --local --command "SELECT * FROM documents"
# Apply migrations locallywrangler d1 migrations apply trokky-db --localDeployment
Section titled “Deployment”Deploy Worker
Section titled “Deploy Worker”wrangler deployEnvironment Variables
Section titled “Environment Variables”Set secrets:
wrangler secret put JWT_SECRETwrangler secret put ADMIN_PASSWORDOr use wrangler.toml:
[vars]ADMIN_USERNAME = "admin"
# Don't put secrets in wrangler.toml!Migration from Filesystem
Section titled “Migration from Filesystem”import { FilesystemAdapter } from '@trokky/adapter-filesystem';import { CloudflareAdapter } from '@trokky/adapter-cloudflare';
async function migrate(env: Env) { const source = new FilesystemAdapter({ contentDir: './content' }); const target = new CloudflareAdapter({ d1: env.DB, r2: env.STORAGE });
const schemas = ['post', 'author', 'category'];
for (const schema of schemas) { const docs = await source.listDocuments(schema); for (const doc of docs) { await target.createDocument(schema, doc); } }
// Migrate media const media = await source.listMedia(); for (const asset of media) { const file = await fs.readFile(`./uploads/${asset.filename}`); await target.uploadMedia(file, asset); }}Troubleshooting
Section titled “Troubleshooting”D1 Query Errors
Section titled “D1 Query Errors”// Check D1 queryconst result = await env.DB.prepare( 'SELECT * FROM documents WHERE type = ?').bind('post').all();console.log(result);R2 Access Issues
Section titled “R2 Access Issues”// Check R2 objectconst object = await env.STORAGE.get('media/photo-abc123.jpg');if (!object) { console.log('Object not found');}Migration Issues
Section titled “Migration Issues”# Check migration statuswrangler d1 migrations list trokky-dbNext Steps
Section titled “Next Steps”- Filesystem Adapter - Local development
- S3 Adapter - AWS storage
- Deployment Guide - Production deployment