Building robust stateful agents with Effect and Cloudflare Durable Objects
Treating the Durable Object as a thin adapter: Effect runtimes, injectable DoContext, SQLite repositories, and Vitest without Miniflare.
Originally published on Medium (Jan 2026).
Cloudflare Durable Objects are powerful primitives for building stateful applications. They provide a unique combination of persistent storage, WebSocket handling, and single-threaded execution that makes them ideal for AI agents or collaborative tools. However, as logic grows, the standard class-based model can become difficult to manage. State management, side effects, and testing often turn into pain points.
We recently refactored our agent system to use Effect within Durable Objects. The goal was simple: keep the Cloudflare infrastructure code separate from our business logic and make the entire system testable without deploying to the edge.
Here is how we architected it. Keep in mind the examples below are somewhat simplified, mainly without the business logic that is very particular to our codebase. Still very close to the real thing.
The architecture: the DO as a container
The core concept is to treat the Durable Object class strictly as an adapter. It shouldn’t contain business logic. Its only job is to receive events from Cloudflare (HTTP requests, WebSocket messages, Alarms) and forward them to an Effect Runtime.
Each Durable Object instance initializes its own ManagedRuntime. This allows us to maintain the stateful nature of the DO while gaining the dependency injection and error handling capabilities of Effect.

1. The Durable Object “shell”
The implementation starts with the Durable Object class. Notice how little code is here. It creates the runtime in the constructor and delegates every method call.

By using runFork, we fire off Effects asynchronously without blocking the DO’s main thread logic, matching how you typically handle events in a DO.
2. Injecting Cloudflare capabilities
One challenge with Durable Objects is that state, env, and storage are only available inside the class instance. To use them in our Effect services, we treat them as dependencies.
We define a DoContext layer that holds these references.

Now, any service that needs to read from storage or access environment variables simply requires DoContext.
3. Storage with SQLite
Durable Objects recently gained native SQLite support. Instead of scattering SQL strings throughout our logic, we wrap the database interaction in a Repository service.
This service pulls the SQL interface from the DoContext and exposes clean Effect-based methods.

This separation allows us to test our logic against an in-memory database mock without needing a real Cloudflare environment.
4. Lifecycle management
The AgentRuntime service acts as the entry point for the logic. It defines what happens during the lifecycle events we delegated from the DO.

5. Testability and composition
The biggest win from this architecture is testability. Because the Durable Object is just a shell around a constructed Layer, we can test the entire runtime logic by providing test layers.
We don’t need miniflare or wrangler dev to verify our business logic. We can write standard Vitest tests that simulate the environment.

Summary
This approach decouples the “what” (agent logic, state management, messaging) from the “where” (Cloudflare Durable Objects).
The benefits are clear:
- Decoupling for Cloudflare primitives: makes it much easier to test and allows reusing the runtime in a different environment if you decide to.
- Focus: when working with agents you don’t need to think about Cloudflare peculiarities; that stays under the layers that handle integration.
- Testability: the logic is testable in isolation.
By injecting the platform capabilities (SQL, WebSocket API) as dependencies, we get a robust, portable runtime that scales with complexity.