JSPM

effectful-cloudflare

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 11
  • Score
    100M100P100Q66398F
  • License MIT

Type-safe Effect v4 bindings for Cloudflare Workers platform services

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.

Status: Alpha.

Features

  • Effect v4 nativeServiceMap.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.fn for automatic spans and stack traces
  • Tagged errors — Precise error types for every operation (serializable + internal)
  • Test-friendly — In-memory mocks for all services (Testing module)
  • Zero REST overhead — Direct binding usage, no network calls where native APIs exist

Installation

npm install effectful-cloudflare
# or
bun add effectful-cloudflare

Peer dependency: effect: ^4.0.0-beta

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)
  • LayerMap lets 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() — KV
  • Testing.memoryD1() — D1
  • Testing.memoryR2() — R2
  • Testing.memoryQueue() — Queue
  • Testing.memoryCache() — Cache
  • Testing.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

  1. Explicit over implicit — Services require bindings at construction. No runtime surprises.
  2. Type-safe by default — All bindings are structurally typed. Mock-friendly.
  3. Schema-first — JSON serialization built-in, schema validation optional.
  4. Composable — Single-instance (Layer) + multi-instance (LayerMap) patterns.
  5. Traceable — All methods use Effect.fn for automatic spans.
  6. Tagged errors — Precise error types for every operation.
  7. 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.json

Requirements

  • Effect: ^4.0.0-beta
  • TypeScript: ^5.9
  • Cloudflare Workers: Latest (2024+)

License

MIT © itsbroly

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.