A production-ready monorepo for building Cloudflare Workers with Effect-TS, featuring shared domain models, type-safe API contracts, and database integration.
pnpm install # Install dependencies
pnpm build # Build all packages
pnpm check # Type check
pnpm test # Run tests
# Local development
cd apps/effect-worker-api
pnpm dev # Start dev server┌─────────────────────────────────────────────────────────────────┐
│ Applications │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ effect-worker-api │ │ effect-worker-rpc │ │
│ │ (HTTP REST) │ │ (RPC JSON) │ │
│ └──────────────────────┘ └──────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Shared Packages │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ domain │ │ contracts │ │cloudflare │ │ db │ │
│ │ (types) │ │ (API) │ │ (infra) │ │ (schema) │ │
│ └───────────┘ └───────────┘ └───────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
All packages use the @repo/* namespace for internal monorepo imports.
Core domain types, branded schemas, and errors.
import { UserId, UserSchema, UserNotFoundError } from "@repo/domain"
// Branded types for type-safe IDs
const id: UserId = "usr_abc123" as UserIdAPI definitions for HTTP and RPC endpoints. Defines the contract between client and server.
import { WorkerApi, UsersGroup, UsersRpc } from "@repo/contracts"HTTP Groups:
HealthGroup- Health check endpointsUsersGroup- User CRUD operations
RPC Procedures:
UsersRpc- User operations via RPC
Infrastructure layer for Cloudflare Workers integration with Effect.
import {
withCloudflareBindings, // Wrap effects with env/ctx
CloudflareBindings, // Service tag
PgDrizzle, // Database connection
currentEnv, // FiberRef for env access
} from "@repo/cloudflare"Drizzle ORM schema definitions.
import { users } from "@repo/db"REST HTTP API built with @effect/platform.
cd apps/effect-worker-api
pnpm dev # Local dev server
pnpm deploy # Deploy to CloudflareEndpoints:
GET /health- Health checkGET /users- List usersGET /users/:id- Get user by IDPOST /users- Create user
RPC API using @effect/rpc for procedure-based communication.
cd apps/effect-worker-rpc
pnpm dev # Local dev server
pnpm deploy # Deploy to CloudflareEndpoints:
GET /health- Health checkPOST /rpc- RPC endpoint
Request-scoped Cloudflare bindings via Effect's FiberRef:
// Entry point wraps effect with bindings
const effect = handleRequest(request).pipe(withCloudflareBindings(env, ctx))
return runtime.runPromise(effect)
// Handlers access via service
Effect.gen(function* () {
const { env } = yield* CloudflareBindings
// Use env.MY_KV, env.MY_R2, etc.
})Contracts define abstract middleware tags, apps provide implementations:
// In contracts (abstract)
export class DatabaseMiddleware extends HttpApiMiddleware.Tag<DatabaseMiddleware>()(
"DatabaseMiddleware",
{ failure: DatabaseConnectionError, provides: PgDrizzle }
) {}
// In app (implementation)
export const DatabaseMiddlewareLive = Layer.effect(
DatabaseMiddleware,
Effect.gen(function* () {
const drizzle = yield* makeDrizzle()
return drizzle
})
)Type-safe handlers using Effect generators:
export const UsersGroupLive = HttpApiBuilder.group(
WorkerApi,
"users",
(handlers) => handlers
.handle("list", () => Effect.gen(function* () {
const drizzle = yield* PgDrizzle
return yield* drizzle.select().from(users)
}))
.handle("get", ({ path: { id } }) => Effect.gen(function* () {
const drizzle = yield* PgDrizzle
const user = yield* drizzle.select().from(users).where(eq(users.id, id))
if (!user) return yield* Effect.fail(new UserNotFoundError({ id }))
return user
}))
)Typed errors with automatic HTTP status mapping:
export class UserNotFoundError extends S.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{ id: UserIdSchema, message: S.String },
HttpApiSchema.annotations({ status: 404 })
) {}effect-worker-mono/
├── apps/
│ ├── effect-worker-api/ # HTTP REST API
│ │ ├── src/
│ │ │ ├── index.ts # Worker entry point
│ │ │ ├── runtime.ts # Effect runtime
│ │ │ ├── handlers/ # Handler implementations
│ │ │ └── services/ # Middleware implementations
│ │ └── wrangler.jsonc # Cloudflare config
│ └── effect-worker-rpc/ # RPC API
├── packages/
│ ├── domain/ # Domain types & schemas
│ │ └── src/
│ │ ├── schemas/ # Branded types
│ │ └── errors/ # Domain errors
│ ├── contracts/ # API definitions
│ │ └── src/
│ │ ├── http/ # HTTP endpoints
│ │ └── rpc/ # RPC procedures
│ ├── cloudflare/ # Worker infrastructure
│ │ └── src/
│ │ ├── fiber-ref.ts # FiberRef bridge
│ │ ├── services.ts # Service tags
│ │ └── database.ts # Connection factory
│ └── db/ # Database schema
│ └── src/schema.ts # Drizzle tables
└── reports/ # Architecture decisions
Strict mode enabled with path aliases for all packages:
{
"compilerOptions": {
"paths": {
"@repo/domain": ["./packages/domain/src"],
"@repo/contracts": ["./packages/contracts/src"],
"@repo/cloudflare": ["./packages/cloudflare/src"],
"@repo/db": ["./packages/db/src"]
}
}
}Configure in wrangler.jsonc:
Cloudflare Hyperdrive provides connection pooling for PostgreSQL. In production, configure via wrangler.jsonc:
{
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "your-hyperdrive-id" }]
}For local development, Wrangler simulates Hyperdrive using an environment variable. Create a .env file or export:
# .dev.vars (or export in shell)
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@localhost:5432/effect_worker"The HYPERDRIVE suffix must match your binding name. Wrangler will automatically provide env.HYPERDRIVE.connectionString in your worker.
Usage in middleware:
return yield* makeDrizzle(env.HYPERDRIVE.connectionString)| Command | Description |
|---|---|
pnpm build |
Build all packages |
pnpm check |
Type check all packages |
pnpm test |
Run all tests |
pnpm coverage |
Generate coverage report |
pnpm clean |
Remove dist folders |
| Category | Technology |
|---|---|
| Runtime | Cloudflare Workers |
| Framework | Effect-TS |
| HTTP | @effect/platform |
| RPC | @effect/rpc |
| Database | Drizzle ORM + PostgreSQL |
| Build | pnpm workspaces + TypeScript |
| Testing | Vitest + @effect/vitest |
| Deployment | Wrangler |
- effect - Core functional effects runtime
- @effect/platform - HTTP server & middleware
- @effect/rpc - RPC protocol
- @effect/sql-drizzle - Database integration
- drizzle-orm - Type-safe ORM
- wrangler - Cloudflare Workers CLI
- Make changes to packages or apps
- Build packages if contract/domain/infra changed:
pnpm build - Type check:
pnpm check - Run tests:
pnpm test - Dev server:
cd apps/effect-worker-api && pnpm dev - Deploy:
pnpm deploy
cd packages/db
# Push schema changes
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:push
# Open Drizzle Studio
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:studio
# Generate migrations
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:generate
# Run migrations
DATABASE_URL=postgres://postgres:postgres@localhost:5432/effect_worker pnpm db:migrateSee LICENSE for details.
{ "kv_namespaces": [{ "binding": "MY_KV", "id": "xxx" }], "r2_buckets": [{ "binding": "MY_R2", "bucket_name": "xxx" }], "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "xxx" }] }