npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

effectful-cloudflare

v0.4.1

Published

Type-safe Effect v4 bindings for Cloudflare Workers platform services

Readme

effectful-cloudflare

Type-safe Effect v4 bindings for Cloudflare Workers platform services.

Warning: Alpha release, depends on Effect v4 (beta). API may change before 1.0.0.

Features

  • Effect v4 nativeContext.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
  • 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

Installation

npm install effectful-cloudflare

Peer 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 ~3 KB gzipped. The Testing module (7.5 KB) is only imported when explicitly needed.

Quick Start

import { Effect, Layer } from "effect"
import { KV } from "effectful-cloudflare/KV"
import { serve } from "effectful-cloudflare/Worker"

const handler = (request: Request) => Effect.gen(function*() {
  const kv = yield* KV
  const user = yield* kv.get("user:123")
  return new Response(JSON.stringify(user))
})

export default 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*() {
  const kv = yield* KV.make(env.MY_KV)
  const value = yield* kv.get("key")
  yield* kv.put("key", { data: "value" })
})

// With schema validation:
const kv = yield* KV.make(env.MY_KV, UserSchema)

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"

const kvLayer = KV.layer(env.MY_KV)

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

Schema validation varies by service, applied where it makes the most sense:

Construction-time schema (KV, Queue, Cache)

For homogeneous stores where one namespace = one data type:

import { Schema } from "effect"
import { KV } from "effectful-cloudflare/KV"

const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  email: Schema.String,
})

// Factory — schema as second argument
const kv = yield* KV.make(env.MY_KV, User)
yield* kv.put("user:123", { id: "123", name: "Alice", email: "[email protected]" }) // typechecked
const user = yield* kv.get("user:123") // User | null

// Layer — same API
const kvLayer = KV.layer(env.MY_KV, User)

Queue uses a factory method for schema:

import { QueueProducer } from "effectful-cloudflare/Queue"

const typedQueue = QueueProducer.json(TaskSchema)
const layer = typedQueue.layer(env.MY_QUEUE)

Per-call schema (D1, AI, DOClient)

For heterogeneous services where each call returns a different type:

import { D1 } from "effectful-cloudflare/D1"

const db = yield* D1
const users = yield* db.querySchema(User, "SELECT * FROM users WHERE active = ?", true)
const user = yield* db.queryFirstSchema(User, "SELECT * FROM users WHERE id = ?", 123)
import { AI } from "effectful-cloudflare/AI"

const ai = yield* AI
const result = yield* ai.runSchema("@cf/meta/llama-3-8b-instruct", ResponseSchema, { prompt: "..." })

Multi-Instance Pattern (LayerMap)

Problem: Your Worker has multiple KV namespaces (env.KV_USERS, env.KV_CACHE) and you need to use different ones in different parts of your app.

Solution: Use LayerMap to dynamically resolve which binding to use by name.

// wrangler.jsonc
{
  "kv_namespaces": [
    { "binding": "KV_USERS", "id": "..." },
    { "binding": "KV_CACHE", "id": "..." }
  ]
}

The library provides built-in KVMap, D1Map, R2Map, and QueueProducerMap LayerMap services. These require WorkerEnv to resolve bindings by name:

import { Effect, Layer } from "effect"
import { KV, KVMap } from "effectful-cloudflare/KV"
import { WorkerEnv } from "effectful-cloudflare/Worker"

// Provide WorkerEnv + KVMap layers
const layers = Layer.mergeAll(
  WorkerEnv.layer(env),
  KVMap.layer
)

// Use different KV namespaces dynamically
const program = Effect.gen(function*() {
  // Access KV_USERS namespace
  const usersKV = yield* KV.pipe(Effect.provide(KVMap.get("KV_USERS")))
  const user = yield* usersKV.get("user:123")

  // Access KV_CACHE namespace
  const cacheKV = yield* KV.pipe(Effect.provide(KVMap.get("KV_CACHE")))
  const cached = yield* cacheKV.get("result:abc")
}).pipe(Effect.provide(layers))

You can also define a custom LayerMap with your own lookup logic:

class MyKVMap extends LayerMap.Service<MyKVMap>()("app/KVMap", {
  lookup: (bindingName: string) =>
    Layer.effect(KV,
      Effect.gen(function*() {
        const env = yield* WorkerEnv
        return yield* KV.make(env[bindingName] as KVBinding)
      })
    ),
  idleTimeToLive: "5 minutes",
}) {}

Use LayerMap when: Multiple bindings of the same type with different purposes, or dynamic binding selection at runtime.

Don't use when: One binding per service type (use KV.layer(env.MY_KV)) or partitioning data within one KV (use key prefixes).

Error Handling

All services use tagged errors:

import { Effect } 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

  yield* kv.put("user:123", { id: "123", name: "Alice" })
  const user = yield* kv.get("user:123")
  const result = yield* kv.getWithMetadata("user:123")
  const keys = yield* kv.list({ prefix: "user:" })
  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 (per-call)
  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

  yield* r2.put("file.txt", "Hello, world!", {
    httpMetadata: { contentType: "text/plain" },
    customMetadata: { author: "Alice" },
  })

  const obj = yield* r2.get("file.txt")
  const obj2 = yield* r2.getOrFail("file.txt")
  const info = yield* r2.head("file.txt")
  const list = yield* r2.list({ prefix: "uploads/" })

  // Multipart upload
  const upload = yield* r2.createMultipartUpload("large.bin")
  // ... upload parts ...
  yield* upload.complete([...uploadedParts])

  yield* r2.delete("file.txt")
})

Queue — Message Queue

import { Schema } from "effect"
import { QueueProducer, consume } from "effectful-cloudflare/Queue"

const Message = Schema.Struct({
  type: Schema.Literal("user.created"),
  userId: Schema.String,
})

// Producer — with schema validation via .json() factory
const program = Effect.gen(function*() {
  const queue = yield* QueueProducer
  yield* queue.send({ type: "user.created", userId: "123" })
  yield* queue.sendBatch([
    { body: { type: "user.created", userId: "123" } },
    { body: { type: "user.created", userId: "456" } },
  ])
})

// Consumer — standalone function, returns CF handler
export default {
  ...consume({ schema: Message }).handler((message, meta) =>
    Effect.gen(function*() {
      console.log("Received:", message)
    })
  ),
}

Durable Objects

import { Effect } from "effect"
import { DOClient, EffectDurableObject } from "effectful-cloudflare/DurableObject"

// Server: Define DO class
export class Counter extends EffectDurableObject {
  fetch(request: Request) {
    return Effect.gen(this, function*(self) {
      const count = yield* self.storage.get("count").pipe(
        Effect.map((v) => (v as number) ?? 0)
      )
      yield* self.storage.put("count", count + 1)
      return new Response(JSON.stringify({ count: count + 1 }))
    })
  }

  alarm() {
    return Effect.gen(this, function*(self) {
      console.log("Alarm triggered!")
    })
  }
}

// Client: Call DO from worker
const program = Effect.gen(function*() {
  const client = yield* DOClient
  const stub = yield* client.stub(env.COUNTER, { type: "name", name: "global" })
  const response = yield* client.fetch(stub, new Request("https://counter/"))
  const data = yield* Effect.promise(() => response.json())
  console.log(data.count)
}).pipe(Effect.provide(DOClient.layer()))

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 per-call schema validation
  const result2 = yield* ai.runSchema(
    "@cf/meta/llama-3-8b-instruct",
    Response,
    { prompt: "What is the capital of France?" }
  )
})

Worker Entrypoint

import { Effect, Layer } from "effect"
import { serve, onScheduled, onQueue, ExecutionCtx } 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
  return new Response("OK")
})

// Compose layers
const makeAppLayer = (env: Env, ctx: ExecutionContext) =>
  Layer.mergeAll(
    KV.layer(env.MY_KV),
    D1.layer(env.MY_DB),
    ExecutionCtx.layer(ctx),
  )

// Export handlers
export default {
  ...serve(handler, makeAppLayer),
  ...onScheduled((controller) => Effect.gen(function*() {
    const kv = yield* KV
    // Run scheduled task...
  }), makeAppLayer),
  ...onQueue((batch) => Effect.gen(function*() {
    const db = yield* D1
    // Process queue messages...
  }), makeAppLayer),
}

Testing

All services have in-memory mocks:

import { Effect } from "effect"
import { describe, it } from "vitest"
import { KV } from "effectful-cloudflare/KV"
import * as Testing from "effectful-cloudflare/Testing"

describe("KV", () => {
  it("should get and put values", () =>
    Effect.gen(function*() {
      const kv = yield* KV
      yield* kv.put("key", { value: "test" })
      const result = yield* kv.get("key")
      expect(result).toEqual({ value: "test" })
    }).pipe(
      Effect.provide(KV.layer(Testing.memoryKV()))
    )
  )
})

Available mocks:

| Mock | Service | |------|---------| | Testing.memoryKV() | KV | | Testing.memoryD1() | D1 | | Testing.memoryR2() | R2 | | Testing.memoryQueue() | QueueProducer | | Testing.memoryCache() | Cache | | Testing.memoryDOStorage() | Durable Object storage | | Testing.memoryVectorize() | Vectorize | | Testing.memoryAI() | AI | | Testing.memoryAIGateway() | AIGateway | | Testing.memoryBrowser() | Browser | | Testing.memoryPipeline() | Pipeline |

Error Types

Shared Errors (effectful-cloudflare/Errors)

| Error | Kind | Description | |-------|------|-------------| | BindingError | Data.TaggedError (internal) | Binding not available in worker environment | | SchemaError | Schema.TaggedErrorClass (serializable) | Schema encode/decode failed | | NotFoundError | Schema.TaggedErrorClass (serializable, HTTP 404) | Resource not found |

Module-Specific Errors

  • KV: KVError
  • D1: D1Error, D1QueryError, D1MigrationError
  • R2: R2Error, R2MultipartError, R2PresignError
  • Queue: QueueSendError, QueueConsumerError
  • DurableObject: DOError, StorageError, AlarmError, SqlError, WebSocketError
  • Cache: CacheError
  • AI: AIError
  • AIGateway: AIGatewayRequestError, AIGatewayResponseError
  • Vectorize: VectorizeError
  • 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 nativeContext.Service, Effect.fn, LayerMap. No v3 patterns.

Project Structure

effectful-cloudflare/
├── src/
│   ├── index.ts           # Root barrel exports
│   ├── Errors.ts          # Shared error types
│   ├── Worker.ts          # Worker entrypoint (serve, onScheduled, onQueue)
│   ├── KV.ts              # Workers KV
│   ├── D1.ts              # D1 Database
│   ├── R2.ts              # R2 Object Storage
│   ├── Queue.ts           # Queues (QueueProducer + consume/consumeEffect)
│   ├── DurableObject.ts   # Durable Objects (DOClient + EffectDurableObject)
│   ├── 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/
├── docs/
└── package.json

Requirements

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

License

MIT

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 | Cloudflare REST Management API (api.cloudflare.com) | | Runs where | Inside a Cloudflare Worker | Inside a Cloudflare Worker | Anywhere (Node, Bun, CLI, CI) | | Effect version | v4 (Context.Service, LayerMap, Effect.fn) | v3 (Context.Tag, @effect/schema) | v3-era (Context.GenericTag) |

Service Coverage

| Service | effectful-cloudflare | effect-cf | distilled-cloudflare | |---------|--------------------------|---------------|--------------------------| | KV (runtime) | Yes | Yes | No (namespace mgmt via REST) | | D1 (SQL) | Yes | Yes | No (database mgmt via REST) | | R2 (objects) | Yes | Yes | No (bucket mgmt via REST) | | Queue (send/consume) | Yes | Yes | No (queue mgmt via REST) | | Durable Objects | Yes (client + server + storage) | Yes | 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 | No (infra-level) | No | Yes (30 admin API services) | | Worker entrypoint | Yes (serve, onScheduled, onQueue) | Yes | No | | Test mocks | Yes (11 services) | Yes | No |

When to Use Which

| Use case | Recommended | |----------|-------------| | Building an app inside a Cloudflare Worker | effectful-cloudflare | | Effect v3 project already using effect-cf | effect-cf (or migrate for v4) | | Managing CF infrastructure from CLI/CI | distilled-cloudflare | | Both runtime bindings AND admin API | effectful-cloudflare + distilled-cloudflare |