@roostjs/cloudflare Guides
Task-oriented instructions for Cloudflare bindings: D1, R2, KV, Queues, Workers AI, and Vectorize.
How to configure Cloudflare bindings in wrangler.jsonc
Declare all bindings in wrangler.jsonc so they appear in your Worker's env object at runtime.
{
"name": "my-app",
"compatibility_date": "2024-01-01",
"d1_databases": [
{ "binding": "DB", "database_name": "my-app-db", "database_id": "..." }
],
"kv_namespaces": [
{ "binding": "CACHE", "id": "..." }
],
"r2_buckets": [
{ "binding": "FILES", "bucket_name": "my-app-files" }
],
"queues": {
"producers": [{ "binding": "MY_QUEUE", "queue": "my-app-queue" }],
"consumers": [{ "queue": "my-app-queue", "max_batch_size": 10 }]
},
"ai": {
"binding": "AI"
},
"vectorize": [
{ "binding": "VECTORIZE", "index_name": "my-embeddings" }
]
}Create D1 databases and KV namespaces via the Cloudflare dashboard or wrangler CLI before referencing their IDs here.
How to use D1 for database queries
Wrap the raw D1 binding with D1Database for typed prepared statements and batch operations.
import { D1Database } from '@roostjs/cloudflare';
const db = new D1Database(env.DB);
// Parameterized query
const stmt = db.prepare('SELECT * FROM users WHERE id = ?1');
const row = await stmt.bind(1).first();
// Fetch multiple rows
const rows = await db.prepare('SELECT * FROM users WHERE active = ?1')
.bind(true)
.all();
// Batch multiple statements atomically
const results = await db.batch([
db.prepare('INSERT INTO users (name, email) VALUES (?1, ?2)').bind('Alice', 'alice@example.com'),
db.prepare('INSERT INTO audit_log (event) VALUES (?1)').bind('user.created'),
]);For application-level querying, prefer @roostjs/orm models over raw D1. Use raw D1 for migrations or complex SQL not covered by the query builder.
How to store and retrieve files with R2
Use R2Storage to upload and download objects. Pass a contentType in httpMetadata so browsers render files correctly.
import { R2Storage } from '@roostjs/cloudflare';
const storage = new R2Storage(env.FILES);
// Upload from a form file input
const formData = await request.formData();
const file = formData.get('avatar') as File;
const buffer = await file.arrayBuffer();
await storage.put(`avatars/${userId}.png`, buffer, {
httpMetadata: { contentType: file.type },
});
// Download and stream to client
const object = await storage.get(`avatars/${userId}.png`);
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: { 'content-type': object.httpMetadata?.contentType ?? 'application/octet-stream' },
});
// Delete
await storage.delete(`avatars/${userId}.png`);
// Check existence without downloading
const meta = await storage.head(`avatars/${userId}.png`);
const exists = meta !== null;How to use KV for caching
Use KVStore for short-lived cache entries. Always set expirationTtl to avoid stale data accumulating.
import { KVStore } from '@roostjs/cloudflare';
const kv = new KVStore(env.CACHE);
// Cache-aside pattern
async function getUser(id: string) {
const cached = await kv.get<User>(`user:${id}`, 'json');
if (cached) return cached;
const user = await User.findOrFail(id);
await kv.putJson(`user:${id}`, user, { expirationTtl: 3600 }); // 1 hour
return user;
}
// Invalidate on update
async function updateUser(id: string, data: Partial<User>) {
await user.save();
await kv.delete(`user:${id}`);
}
// List keys with a prefix
const result = await kv.list({ prefix: 'user:', limit: 100 });
for (const key of result.keys) {
await kv.delete(key.name);
}How to send messages to a Queue
Use QueueSender for direct queue access, or use @roostjs/queue jobs for structured background processing.
import { QueueSender } from '@roostjs/cloudflare';
const queue = new QueueSender(env.MY_QUEUE);
// Single message
await queue.send({ userId: 'user_123', action: 'send-welcome-email' });
// Batch messages (more efficient for bulk operations)
await queue.sendBatch([
{ body: { userId: 'user_1' }, contentType: 'application/json' },
{ body: { userId: 'user_2' }, contentType: 'application/json' },
{ body: { userId: 'user_3' }, contentType: 'application/json' },
]);How to use Workers AI
Use AIClient to run inference on Cloudflare's hosted models. No API keys required — the AI binding in wrangler.jsonc is all you need.
import { AIClient } from '@roostjs/cloudflare';
const ai = new AIClient(env.AI);
// Text generation
const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Summarize this text: ...' },
],
});
// Embeddings (for semantic search / Vectorize)
const embeddings = await ai.run('@cf/baai/bge-base-en-v1.5', {
text: ['Hello world', 'How are you?'],
});For full agent capabilities with tools and conversation memory, use @roostjs/ai. AIClient is the low-level binding wrapper.
How to configure Vectorize for embeddings
Create a Vectorize index in the Cloudflare dashboard matching the dimensionality of your embedding model, then use VectorStore to insert and query.
import { VectorStore, AIClient } from '@roostjs/cloudflare';
const vectorize = new VectorStore(env.VECTORIZE);
const ai = new AIClient(env.AI);
// Generate embeddings and insert
async function indexDocument(id: string, text: string) {
const result = await ai.run('@cf/baai/bge-base-en-v1.5', { text: [text] });
const vector = result.data[0];
await vectorize.insert([{
id,
values: vector,
metadata: { text, createdAt: new Date().toISOString() },
}]);
}
// Query for similar documents
async function semanticSearch(query: string, topK = 5) {
const result = await ai.run('@cf/baai/bge-base-en-v1.5', { text: [query] });
const queryVector = result.data[0];
const matches = await vectorize.query(queryVector, { topK, returnMetadata: true });
return matches.matches;
}
// Delete by IDs
await vectorize.deleteByIds(['doc_1', 'doc_2']);The Vectorize index dimension must match your embedding model's output size. bge-base-en-v1.5 outputs 768 dimensions.
How to call another Worker with ServiceClient
Declare a service binding in wrangler.jsonc, then resolve a ServiceClient from the
container. CloudflareServiceProvider auto-detects Fetcher bindings and wraps them.
{
"services": [
{ "binding": "PAYMENTS_SERVICE", "service": "payments-worker" }
]
}import { ServiceClient } from '@roostjs/cloudflare';
// Resolved automatically by CloudflareServiceProvider
const payments = container.resolve<ServiceClient>('PAYMENTS_SERVICE');
const response = await payments.post('/charge', {
amount: 2000,
currency: 'usd',
customerId: 'cus_123',
});
if (!response.ok) {
throw new Error(`Payment failed: ${await response.text()}`);
}Use call() for typed RPC when the remote Worker follows the /rpc/{method} convention:
const result = await payments.call<{ chargeId: string }>('createCharge', {
amount: 2000,
currency: 'usd',
});How to dispatch to customer Workers with DispatchNamespaceClient
Workers for Platforms lets you dispatch inbound requests to tenant-owned scripts. Declare
a dispatch namespace binding, then use DispatchNamespaceClient to route by script name.
{
"dispatch_namespaces": [
{ "binding": "DISPATCH", "namespace": "customer-scripts" }
]
}import { DispatchNamespaceClient } from '@roostjs/cloudflare';
const dispatch = container.resolve<DispatchNamespaceClient>('DISPATCH');
// Get a ServiceClient for a specific tenant script
const tenantClient = dispatch.dispatchClient(tenantScriptName);
const response = await tenantClient.get('/status');For full control, use dispatch() to get the raw Fetcher:
const fetcher = dispatch.dispatch(tenantScriptName, { trust: 'trusted' });
const response = await fetcher.fetch('https://worker/path');How to communicate with a Cloudflare Container
ContainerClient wraps a DurableObjectNamespace binding that fronts a container
deployment. Declare the DO binding in wrangler.jsonc and reference it by name.
import { ContainerClient } from '@roostjs/cloudflare';
const containers = container.resolve<ContainerClient>('RENDER_CONTAINER');
// Optional: pre-warm before serving traffic
const ready = await containers.warmup('renderer-1');
// Send a request to the named container instance
const response = await containers.send('renderer-1', '/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template: 'invoice', data: invoiceData }),
});Each unique name maps to a separate container instance via idFromName. Roost does not
automatically register ContainerClient — instantiate it directly if your binding is not
auto-detected as a DurableObjectNamespace.
How to transform HTML responses at the edge
Use HtmlTransformer to modify HTML as it streams through the Worker. Chain methods to
compose transformations, then call transform() on the response.
import { HtmlTransformer } from '@roostjs/cloudflare';
export async function enrichHtml(
request: Request,
next: (r: Request) => Promise<Response>
): Promise<Response> {
const response = await next(request);
if (!response.headers.get('content-type')?.includes('text/html')) {
return response;
}
return new HtmlTransformer()
.injectScript('/dist/analytics.js')
.setMetaTag('og:site_name', 'Acme Corp')
.transform(response);
}For A/B testing, provide a variant map and an assignment function that reads a cookie or header to select the variant:
const transformer = new HtmlTransformer().abTest(
'#hero-cta',
{
control: '<a href="/signup">Get started</a>',
variant_a: '<a href="/signup">Start free trial</a>',
},
(req) => getCookieVariant(req) ?? 'control'
);
return transformer.transform(response, request);How to use VersionedKVStore for content-addressed caching
VersionedKVStore is suited to data that changes infrequently and where you want
cache-hit detection by hash rather than by TTL.
import { VersionedKVStore, KVStore } from '@roostjs/cloudflare';
const kv = container.resolve<KVStore>('CONFIG_KV');
const store = new VersionedKVStore(kv, { contentTtl: 3600 });
// Write — returns the SHA-256 content hash
const hash = await store.put('app-config', { featureFlags: { darkMode: true } });
// Read
const config = await store.get<AppConfig>('app-config');
// Skip re-fetching if the caller already has the current version
const currentHash = await store.getVersion('app-config');
if (currentHash === localHash) {
// Already up to date
} else {
const fresh = await store.get<AppConfig>('app-config');
}VersionedKVStore uses last-writer-wins semantics. Do not use it for keys with concurrent
writes from multiple isolates without external coordination.
How to add rate limiting
KV-backed rate limiting
KVRateLimiter uses a fixed window per IP and is appropriate for moderate-traffic
Workers where eventual consistency is acceptable.
import { Application } from '@roostjs/core';
import { KVRateLimiter, KVStore } from '@roostjs/cloudflare';
const app = Application.create(env);
app.useMiddleware(
new KVRateLimiter(new KVStore(env.RATE_LIMIT_KV), {
limit: 100,
window: 60, // 100 requests per 60 seconds per IP
})
);Durable Object-backed rate limiting
DORateLimiter routes checks to a named Durable Object instance, providing stronger
consistency under concurrent requests from the same key.
import { DORateLimiter, DurableObjectClient } from '@roostjs/cloudflare';
app.useMiddleware(
new DORateLimiter(new DurableObjectClient(env.RATE_LIMITER_DO), {
limit: 50,
window: 60,
keyExtractor: (req) => req.headers.get('Authorization') ?? 'anonymous',
})
);Export RateLimiterDO from your Worker entry and declare the binding:
{
"durable_objects": {
"bindings": [{ "name": "RATE_LIMITER_DO", "class_name": "RateLimiterDO" }]
}
}export { RateLimiterDO } from '@roostjs/cloudflare';Testing rate limiting
Use fakeRateLimiter() to replace KV and DO checks with an in-memory fake:
import { fakeRateLimiter, restoreRateLimiter } from '@roostjs/cloudflare';
test('returns 429 when rate limited', async () => {
const fake = fakeRateLimiter();
fake.limitKey('192.0.2.1');
const response = await app.handle(
new Request('https://example.com/api', {
headers: { 'CF-Connecting-IP': '192.0.2.1' },
})
);
expect(response.status).toBe(429);
fake.assertLimited('192.0.2.1');
restoreRateLimiter();
});