Package Exports
- effectful-cloudflare
- effectful-cloudflare/AI
- effectful-cloudflare/AIGateway
- effectful-cloudflare/Browser
- effectful-cloudflare/Cache
- effectful-cloudflare/D1
- effectful-cloudflare/DurableObject
- effectful-cloudflare/Errors
- effectful-cloudflare/Hyperdrive
- effectful-cloudflare/KV
- effectful-cloudflare/Pipeline
- effectful-cloudflare/Queue
- effectful-cloudflare/R2
- effectful-cloudflare/Testing
- effectful-cloudflare/Vectorize
- effectful-cloudflare/Worker
Readme
effectful-cloudflare
Type-safe Effect v4 bindings for Cloudflare Workers platform services.
⚠️ Warning: This library is in alpha and depends on Effect v4 (currently in beta). The API may change before reaching stable 1.0.0. Effect v4 APIs are not yet finalized and breaking changes are possible.
Features
- Effect v4 native —
ServiceMap.Service,Effect.fn,Schema.TaggedErrorClass,LayerMap - Type-safe bindings — Structural types for all CF services (KV, D1, R2, Queue, DO, AI, etc.)
- Schema-first data — Built-in JSON serialization + optional schema validation via
Schema - Composable layers — Single-instance (
Layer) + multi-instance (LayerMap) patterns - Traceable — All methods use
Effect.fnfor automatic spans and stack traces - Tagged errors — Precise error types for every operation (serializable + internal)
- Test-friendly — In-memory mocks for all services (
Testingmodule) - Zero REST overhead — Direct binding usage, no network calls where native APIs exist
Installation
npm install effectful-cloudflare
# or
bun add effectful-cloudflarePeer dependency: effect: ^4.0.0-beta
Bundle Size
- Full package: 67 KB (npm tarball)
- Runtime usage: ~3-10 KB gzipped (depending on imports)
- Tree-shakeable: Import only what you need via subpath exports
Example: import { KV } from "effectful-cloudflare/KV" adds only ~3 KB gzipped to your bundle.
The Testing module (7.5 KB) is separate and only imported when explicitly needed for tests.
Quick Start
import { Effect, Layer } from "effect"
import { KV } from "effectful-cloudflare/KV"
import { Worker } from "effectful-cloudflare/Worker"
// Define your worker handler
const handler = (request: Request) => Effect.gen(function*() {
// Access KV service from context
const kv = yield* KV
// Get value (auto-JSON parsed)
const user = yield* kv.get("user:123")
// Return Response
return new Response(JSON.stringify(user))
})
// Export CF Worker handler
export default Worker.serve(handler, (env) => KV.layer(env.MY_KV))Module Catalog
| Module | Import | Description |
|---|---|---|
| Worker | effectful-cloudflare/Worker |
Worker entrypoint (serve, onScheduled, onQueue) |
| KV | effectful-cloudflare/KV |
Workers KV (key-value store) |
| D1 | effectful-cloudflare/D1 |
D1 SQL database (SQLite) |
| R2 | effectful-cloudflare/R2 |
R2 object storage (S3-compatible) |
| Queue | effectful-cloudflare/Queue |
Queues (producer + consumer) |
| DurableObject | effectful-cloudflare/DurableObject |
Durable Objects (client + server + storage) |
| Cache | effectful-cloudflare/Cache |
Cache API |
| AI | effectful-cloudflare/AI |
Workers AI (inference models) |
| AIGateway | effectful-cloudflare/AIGateway |
AI Gateway (multi-provider routing) |
| Vectorize | effectful-cloudflare/Vectorize |
Vectorize (vector database) |
| Hyperdrive | effectful-cloudflare/Hyperdrive |
Hyperdrive (connection pooling) |
| Browser | effectful-cloudflare/Browser |
Browser Rendering |
| Pipeline | effectful-cloudflare/Pipeline |
Pipelines (R2 streaming ETL) |
| Errors | effectful-cloudflare/Errors |
Shared error types |
| Testing | effectful-cloudflare/Testing |
In-memory mocks |
Core Concepts
Service Pattern
Every service can be used in two ways:
1. Factory Pattern (direct usage, no DI)
Create the service directly from the binding and use it immediately:
import { KV } from "effectful-cloudflare/KV"
const program = Effect.gen(function*() {
// Create service from binding and use it directly
const kv = yield* KV.make(env.MY_KV)
const value = yield* kv.get("key")
yield* kv.put("key", { data: "value" })
})Use when: You need the service in a single place and don't need dependency injection.
2. Layer Pattern (dependency injection)
Provide the service as a Layer and access it from the Effect context:
import { KV } from "effectful-cloudflare/KV"
// Create Layer from binding
const kvLayer = KV.layer(env.MY_KV)
// Access service from context
const program = Effect.gen(function*() {
const kv = yield* KV
const value = yield* kv.get("key")
yield* kv.put("key", { data: "value" })
}).pipe(Effect.provide(kvLayer))Use when: You want to compose multiple services or make your code testable (swap layers for mocks).
Schema Validation
All data services support optional schema validation:
import { Schema } from "effect"
import { KV } from "effectful-cloudflare/KV"
// Define schema
const User = Schema.Struct({
id: Schema.String,
name: Schema.String,
email: Schema.String,
})
// Create typed KV service
const kvLayer = KV.layer(env.MY_KV, User)
// Get value (auto-validated)
const program = Effect.gen(function*() {
const kv = yield* KV
const user = yield* kv.get("user:123") // Type: typeof User.Type | null
})Multi-Instance Pattern
Problem: Your Worker has multiple KV namespaces (e.g., env.KV_USERS, env.KV_CACHE, env.KV_SESSIONS) and you need to use different ones in different parts of your app.
Solution: Use LayerMap to dynamically resolve which KV namespace to use by name.
How it works
LayerMap is a keyed resource pool. Think of it like a Map<string, Layer> that creates layers on-demand:
- Multiple KV namespaces = Multiple Cloudflare KV bindings in
wrangler.jsonc:{ "kv_namespaces": [ { "binding": "KV_USERS", "id": "..." }, { "binding": "KV_CACHE", "id": "..." }, { "binding": "KV_SESSIONS", "id": "..." } ] } - Each binding is a separate KV namespace (isolated storage)
LayerMaplets you pick which one to use dynamically at runtime
Example: Multi-tenant app with isolated KV per tenant
import { Layer, LayerMap } from "effect"
import { KV, KVMap } from "effectful-cloudflare/KV"
import { WorkerEnv } from "effectful-cloudflare/Worker"
// Define your KV namespaces in wrangler.jsonc:
// kv_namespaces: [
// { binding: "KV_USERS", id: "..." },
// { binding: "KV_CACHE", id: "..." }
// ]
// Create a LayerMap that looks up KV namespaces by binding name
class MyKVMap extends LayerMap.Service<MyKVMap>()("app/KVMap", {
lookup: (bindingName: string) =>
Layer.effect(KV)(
Effect.gen(function*() {
const env = yield* WorkerEnv
// env[bindingName] = env.KV_USERS or env.KV_CACHE
return yield* KV.make(env[bindingName])
})
),
idleTimeToLive: "5 minutes", // Cache the layer for 5 min
}) {}
// Usage: Access different KV namespaces in the same program
const program = Effect.gen(function*() {
// Get from KV_USERS namespace
const user = yield* KV.use((kv) => kv.get("user:123"))
.pipe(Effect.provide(MyKVMap.get("KV_USERS")))
// Get from KV_CACHE namespace
const cached = yield* KV.use((kv) => kv.get("result:abc"))
.pipe(Effect.provide(MyKVMap.get("KV_CACHE")))
// Both use the same KV service interface, but different storage backends
})When to use LayerMap
✅ Use LayerMap when:
- You have multiple KV/D1/R2 bindings with different purposes (users, cache, logs, etc.)
- You need to dynamically select which binding to use based on runtime data (tenant ID, region, etc.)
- You want to cache layer construction (avoid recreating services repeatedly)
❌ Don't use LayerMap when:
- You only have one binding per service type → Use simple
KV.layer(env.MY_KV)instead - You want to partition data within one KV → Use key prefixes instead (
user:123,cache:abc)
Error Handling
All services use tagged errors:
import { Effect, Match } from "effect"
import { KV, KVError } from "effectful-cloudflare/KV"
import { NotFoundError } from "effectful-cloudflare/Errors"
const program = Effect.gen(function*() {
const kv = yield* KV
// Option 1: getOrFail (fails with NotFoundError)
const user = yield* kv.getOrFail("user:123")
// Option 2: get + null check
const maybe = yield* kv.get("user:123")
if (maybe === null) {
return yield* new NotFoundError({ resource: "KV", key: "user:123" })
}
// Option 3: catchTag
return yield* kv.getOrFail("user:123").pipe(
Effect.catchTag("NotFoundError", () => Effect.succeed({ default: "user" }))
)
})Usage Examples
KV — Key-Value Store
import { Effect } from "effect"
import { KV } from "effectful-cloudflare/KV"
const program = Effect.gen(function*() {
const kv = yield* KV
// Put JSON value (auto-serialized)
yield* kv.put("user:123", { id: "123", name: "Alice" })
// Get value (auto-deserialized)
const user = yield* kv.get("user:123")
// Get with metadata
const result = yield* kv.getWithMetadata("user:123")
console.log(result.value, result.metadata)
// List keys by prefix
const keys = yield* kv.list({ prefix: "user:" })
// Delete
yield* kv.delete("user:123")
})D1 — SQL Database
import { Schema } from "effect"
import { D1 } from "effectful-cloudflare/D1"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String,
})
const program = Effect.gen(function*() {
const db = yield* D1
// Query with schema validation
const users = yield* db.querySchema(
User,
"SELECT * FROM users WHERE active = ?",
true
)
// Query first row
const user = yield* db.queryFirst(
"SELECT * FROM users WHERE id = ?",
123
)
// Or fail if not found
const user2 = yield* db.queryFirstOrFail(
"SELECT * FROM users WHERE id = ?",
456
)
// Batch (atomic)
const stmts = [
db.prepare("INSERT INTO users (name) VALUES (?)", "Alice"),
db.prepare("INSERT INTO users (name) VALUES (?)", "Bob"),
]
yield* db.batch(stmts)
// Run migrations
yield* db.migrate([
{ name: "001_init", sql: "CREATE TABLE users ..." },
{ name: "002_add_email", sql: "ALTER TABLE users ..." },
])
})R2 — Object Storage
import { R2 } from "effectful-cloudflare/R2"
const program = Effect.gen(function*() {
const r2 = yield* R2
// Put object
yield* r2.put("file.txt", "Hello, world!", {
httpMetadata: { contentType: "text/plain" },
customMetadata: { author: "Alice" },
})
// Get object
const obj = yield* r2.get("file.txt")
if (obj) {
const text = yield* Effect.promise(() => obj.text())
console.log(text)
}
// Or fail if not found
const obj2 = yield* r2.getOrFail("file.txt")
// Head (metadata only)
const info = yield* r2.head("file.txt")
// List objects
const list = yield* r2.list({ prefix: "uploads/" })
// Multipart upload
const upload = yield* r2.createMultipartUpload("large.bin")
// ... upload parts ...
yield* upload.complete([...uploadedParts])
// Delete
yield* r2.delete("file.txt")
})Queue — Message Queue
import { Schema } from "effect"
import { Queue } from "effectful-cloudflare/Queue"
const Message = Schema.Struct({
type: Schema.Literal("user.created"),
userId: Schema.String,
})
// Producer
const program = Effect.gen(function*() {
const queue = yield* Queue
// Send single message (JSON)
yield* queue.send({ type: "user.created", userId: "123" })
// Send batch
yield* queue.sendBatch([
{ body: { type: "user.created", userId: "123" } },
{ body: { type: "user.created", userId: "456" } },
])
})
// Consumer (in worker export)
export default {
queue: Queue.consume({ schema: Message }).handler((message, meta) =>
Effect.gen(function*() {
console.log("Received:", message)
// Process message...
})
),
}Durable Objects
import { Effect } from "effect"
import { DOClient, EffectDurableObject } from "effectful-cloudflare/DurableObject"
// Server: Define DO class
export class Counter extends EffectDurableObject {
fetch = Effect.fn("Counter.fetch")(function*(request: Request) {
const storage = this.storage
// Get current count
const count = yield* storage.get("count").pipe(
Effect.map((v) => (v as number) ?? 0)
)
// Increment
yield* storage.put("count", count + 1)
return new Response(JSON.stringify({ count: count + 1 }))
})
// Optional: alarm
alarm = Effect.fn("Counter.alarm")(function*() {
console.log("Alarm triggered!")
})
}
// Client: Call DO from worker
const program = Effect.gen(function*() {
const client = yield* DOClient
// Get stub
const stub = yield* client.stub(env.COUNTER, { type: "name", name: "global" })
// Fetch
const response = yield* client.fetch(stub, new Request("https://counter/"))
const data = yield* Effect.promise(() => response.json())
console.log(data.count)
})AI — Workers AI
import { Schema } from "effect"
import { AI } from "effectful-cloudflare/AI"
const Response = Schema.Struct({
response: Schema.String,
})
const program = Effect.gen(function*() {
const ai = yield* AI
// Run model (untyped)
const result = yield* ai.run("@cf/meta/llama-3-8b-instruct", {
prompt: "What is the capital of France?",
})
// Run model with schema validation
const result2 = yield* ai.runSchema(
"@cf/meta/llama-3-8b-instruct",
Response,
{ prompt: "What is the capital of France?" }
)
console.log(result2.response)
})Worker Entrypoint
import { Effect, Layer } from "effect"
import { Worker } from "effectful-cloudflare/Worker"
import { KV } from "effectful-cloudflare/KV"
import { D1 } from "effectful-cloudflare/D1"
// HTTP handler
const handler = (request: Request) => Effect.gen(function*() {
const kv = yield* KV
const db = yield* D1
// Your business logic...
return new Response("OK")
})
// Scheduled handler (cron)
const scheduled = (controller: ScheduledController) => Effect.gen(function*() {
const kv = yield* KV
// Run scheduled task...
})
// Queue consumer
const queue = (batch: MessageBatch) => Effect.gen(function*() {
const db = yield* D1
// Process queue messages...
})
// Compose layers
const makeAppLayer = (env: Env, ctx: ExecutionContext) =>
Layer.mergeAll(
KV.layer(env.MY_KV),
D1.layer(env.MY_DB),
Worker.ExecutionCtx.layer(ctx),
)
// Export handlers
export default {
fetch: Worker.serve(handler, makeAppLayer),
scheduled: Worker.onScheduled(scheduled, makeAppLayer),
queue: Worker.onQueue(queue, makeAppLayer),
}Testing
All services have in-memory mocks:
import { Effect } from "effect"
import { describe, it } from "vitest"
import { KV } from "effectful-cloudflare/KV"
import { Testing } from "effectful-cloudflare/Testing"
describe("KV", () => {
it("should get and put values", () =>
Effect.gen(function*() {
const kv = yield* KV
// Put
yield* kv.put("key", { value: "test" })
// Get
const result = yield* kv.get("key")
expect(result).toEqual({ value: "test" })
}).pipe(
Effect.provide(KV.layer(Testing.memoryKV()))
)
)
})Available mocks:
Testing.memoryKV()— KVTesting.memoryD1()— D1Testing.memoryR2()— R2Testing.memoryQueue()— QueueTesting.memoryCache()— CacheTesting.memoryDOStorage()— Durable Object storage
Error Types
Shared Errors (effectful-cloudflare/Errors)
// Binding not available (internal)
class BindingError extends Data.TaggedError("BindingError")<{
service: string
message: string
}>
// Native CF exception (internal)
class TransportError extends Data.TaggedError("TransportError")<{
service: string
operation: string
cause: unknown
}>
// Schema validation failed (serializable)
class SchemaError extends Schema.TaggedErrorClass<SchemaError>()(
"SchemaError",
{
message: Schema.String,
cause: Schema.Defect,
}
)
// Resource not found (serializable, HTTP 404)
class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()(
"NotFoundError",
{
resource: Schema.String,
key: Schema.String,
},
{ httpApiStatus: 404 }
)Module-Specific Errors
Each module exports its own error types:
- KV:
KVError - D1:
D1Error,D1QueryError,D1MigrationError - R2:
R2Error,R2MultipartError,R2PresignError - Queue:
QueueError,QueueSendError,QueueConsumerError - DurableObject:
DOError,StorageError,AlarmError,SqlError,WebSocketError - Cache:
CacheError - AI:
AIError,AIModelError - AIGateway:
AIGatewayError,AIGatewayRequestError,AIGatewayResponseError - Vectorize:
VectorizeError,VectorizeNotFoundError - Hyperdrive:
HyperdriveError - Browser:
BrowserError - Pipeline:
PipelineError
API Design Principles
- Explicit over implicit — Services require bindings at construction. No runtime surprises.
- Type-safe by default — All bindings are structurally typed. Mock-friendly.
- Schema-first — JSON serialization built-in, schema validation optional.
- Composable — Single-instance (
Layer) + multi-instance (LayerMap) patterns. - Traceable — All methods use
Effect.fnfor automatic spans. - Tagged errors — Precise error types for every operation.
- Effect v4 native — No v3 patterns.
ServiceMap,Result,Effect.fn,LayerMap.
Project Structure
effectful-cloudflare/
├── src/
│ ├── index.ts # Re-exports all modules
│ ├── Errors.ts # Shared error types
│ ├── Worker.ts # Worker entrypoint
│ ├── KV.ts # Workers KV
│ ├── D1.ts # D1 Database
│ ├── R2.ts # R2 Object Storage
│ ├── Queue.ts # Queues
│ ├── DurableObject.ts # Durable Objects
│ ├── Cache.ts # Cache API
│ ├── AI.ts # Workers AI
│ ├── AIGateway.ts # AI Gateway
│ ├── Vectorize.ts # Vectorize
│ ├── Hyperdrive.ts # Hyperdrive
│ ├── Browser.ts # Browser Rendering
│ ├── Pipeline.ts # Pipelines
│ └── Testing.ts # In-memory mocks
├── test/ # Vitest tests
├── docs/ # Documentation
└── package.jsonRequirements
- Effect:
^4.0.0-beta - TypeScript:
^5.9 - Cloudflare Workers: Latest (2024+)
License
MIT © itsbroly
Links
Contributing
Contributions welcome! Please open an issue or PR.
Acknowledgments
Inspired by effect-cf
Comparison: effectful-cloudflare vs effect-cf vs distilled-cloudflare
Three Effect-based libraries for Cloudflare — each solving a different problem.
At a Glance
| effectful-cloudflare | effect-cf | distilled-cloudflare | |
|---|---|---|---|
| What it wraps | Worker runtime bindings (env.KV, env.DB, etc.) |
Worker runtime bindings (env.KV, env.DB, etc.) |
Cloudflare REST Management API (api.cloudflare.com/client/v4) |
| Runs where | Inside a Cloudflare Worker | Inside a Cloudflare Worker | Anywhere (Node, Bun, CLI, CI scripts) |
| Effect version | v4 (ServiceMap.Service, LayerMap, Effect.fn) |
v3 (Context.Tag, @effect/schema) |
v3-era (Context.GenericTag) |
Service Coverage
| Service | effectful-cloudflare | effect-cf | distilled-cloudflare |
|---|---|---|---|
| KV (runtime read/write) | Yes | Yes | No (namespace management via REST) |
| D1 (SQL queries) | Yes | Yes | No (database management via REST) |
| R2 (object storage) | Yes | Yes | No (bucket management via REST) |
| Queue (send/consume) | Yes | Yes | No (queue management via REST) |
| Durable Objects | Yes (client + server + storage) | Yes (client + server + storage) | No |
| Cache API | Yes | Yes | No |
| Workers AI | Yes | No | No |
| AI Gateway | Yes | Yes | No |
| Vectorize | Yes | Yes | No |
| Hyperdrive | Yes | Yes | No |
| Browser Rendering | Yes | No | No |
| Pipelines | Yes | No | No |
| DNS, Pages, Zones, etc. | No (infra-level, not runtime) | No | Yes (30 admin API services) |
| Worker entrypoint | Yes (serve, onScheduled, onQueue) |
Yes (serve, onSchedule, createConsumer) |
No |
| In-memory test mocks | Yes | Yes | No (integration tests against real API) |
Architecture & Design
| Aspect | effectful-cloudflare | effect-cf | distilled-cloudflare |
|---|---|---|---|
| Service pattern | ServiceMap.Service (v4) |
Context.Tag (v3) |
Context.GenericTag (v3) |
| Multi-instance | LayerMap.Service (named instances) |
Not supported | N/A (params per API call) |
| Tracing | Effect.fn on every method (auto-spans) |
None | None |
| Schema integration | Built-in JSON + optional schema param | Opt-in per module | Mandatory (all types are Schemas) |
| Binding handling | Required at construction (fail-fast) | Optional (fails at call time) | N/A (HTTP client) |
| Error design | Data.TaggedError + Schema.TaggedErrorClass |
Data.TaggedError |
Schema.TaggedError + error categories |
When to Use Which
| Use case | Recommended |
|---|---|
| Building an app that runs inside a Cloudflare Worker | effectful-cloudflare |
| Effect v3 project that already uses effect-cf | effect-cf (or migrate to effectful-cloudflare for v4) |
| Managing CF infrastructure from CLI tools, CI/CD, or external servers | distilled-cloudflare |
| Need both runtime bindings AND admin API | effectful-cloudflare (inside Worker) + distilled-cloudflare (outside Worker) |
effectful-cloudflare and distilled-cloudflare are complementary, not competing — one wraps the in-process runtime bindings, the other wraps the remote management HTTP API. effect-cf is the Effect v3 predecessor to effectful-cloudflare.