Skip to content

Migrating from v1

Alchemy v1 uses async/await with top-level await for orchestration. Alchemy v2 replaces this with Effect generators for type-safe error handling, composable retries, and declarative resource wiring.

Your existing async fetch handlers do not need to change — you can keep them as-is and still get all the benefits of the new engine.

In v1, you create an app with await alchemy(...) and finalize it at the end:

// v1 — alchemy.run.ts
import alchemy from "alchemy";
import { Worker, R2Bucket } from "alchemy/cloudflare";
const app = await alchemy("my-app", {});
const bucket = await R2Bucket("bucket", {});
const worker = await Worker("worker", {
entrypoint: "./src/worker.ts",
bindings: { BUCKET: bucket },
});
console.log(worker.url);
await app.finalize();

In v2, you export a default Alchemy.Stack and use yield* instead of await:

// v2 — alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export const Bucket = Cloudflare.R2Bucket("Bucket");
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { Bucket },
});
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers() },
Effect.gen(function* () {
const worker = yield* Worker;
return { url: worker.url };
}),
);

Key differences:

  • await alchemy("name") + await app.finalize()Alchemy.Stack("name", { providers }, effect)
  • await R2Bucket("name", {})Cloudflare.R2Bucket("name")
  • await Worker("name", { entrypoint })Cloudflare.Worker("name", { main })
  • entrypoint is now called main
  • Resources are declared at the top level, then yield*-ed inside the Stack
  • No more finalize() — the Stack handles lifecycle automatically

Your existing Worker runtime code does not need to change. The async pattern declares bindings on the Worker’s env prop and uses Cloudflare.InferEnv to type the env object:

alchemy.run.ts
export type
type WorkerEnv = Cloudflare.InferEnv<any>
WorkerEnv
=
Cloudflare
.
type Cloudflare.InferEnv = /*unresolved*/ any
InferEnv
<typeof
const Worker: any
Worker
>;
export const
const Worker: any
Worker
=
any
Cloudflare
.
any
Worker
("Worker", {
main: string
main
: "./src/worker.ts",
env: {
Bucket: any;
}
env
: {
type Bucket: any
Bucket
},
});

Your handler stays the same — just update the type import:

src/worker.ts
import type {
import Env
Env
} from "../alchemy.run.ts";
import type {
import WorkerEnv
WorkerEnv
} from "../alchemy.run.ts";
export default {
async
function fetch(request: Request, env: Env): Promise<Response>
fetch
(
request: Request
request
:
interface Request

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
,
env: Env
env
:
import Env
Env
) {
any
async
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> (+2 overloads)
fetch
(
request: Request
request
:
var Request: {
new (input: RequestInfo | URL, init?: RequestInit): Request;
prototype: Request;
}

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
,
env: Env
env
:
import WorkerEnv
WorkerEnv
) {
const
const object: any
object
= await
env: Env
env
.
any
BUCKET
.
any
get
("key");
const
const object: any
object
= await
env: Env
env
.
any
Bucket
.
any
get
("key");
return new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(
const object: any
object
?.
any
body
?? null);
},
};

Cloudflare.InferEnv derives a fully typed env object from the env declared on the Worker. You get type safety on the binding names and their APIs without using Effect in your runtime code.

The CLI commands are the same:

Terminal window
alchemy deploy
alchemy destroy

Your v1 state is not compatible with v2. On your first deploy, Alchemy creates new resources. You should destroy your v1 stack first, then deploy with v2.

When you’re ready, you can switch to Effect-native Workers. This gives you typed errors, composable retries, and Effect’s HttpServer integration.

Instead of declaring env bindings on the resource props, you bind resources in the Worker’s Init phase using yield*:

src/worker.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Effect
Effect
from "effect/Effect";
import {
const HttpServerRequest: Service<HttpServerRequest, HttpServerRequest>

Server-side representation of an incoming HTTP request.

Details

It extends HttpIncomingMessage with request metadata, parsed cookies, multipart accessors, WebSocket upgrade support, and a modify method for creating adjusted request views.

Service tag for the active server-side HTTP request.

When to use

Use to access the request currently being handled by HTTP server routes and middleware.

@categorymodels

@since4.0.0

@categorycontext

@since4.0.0

HttpServerRequest
} from "effect/unstable/http/HttpServerRequest";
import * as
import HttpServerResponse
HttpServerResponse
from "effect/unstable/http/HttpServerResponse";
import {
import Bucket
Bucket
} from "./bucket.ts";
export default {
async
function fetch(request: Request, env: WorkerEnv): Promise<Response>
fetch
(
request: Request<unknown, CfProperties<unknown>>
request
:
interface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
,
env: WorkerEnv
env
:
type WorkerEnv = /*unresolved*/ any
WorkerEnv
) {
const
const object: any
object
= await
env: WorkerEnv
env
.
any
Bucket
.
any
get
("key");
return new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(
const object: any
object
?.
any
body
?? null);
},
};
export default
import Cloudflare
Cloudflare
.
const Worker: <Cloudflare.WorkerShape, never, Cloudflare.WorkerServices | PlatformServices>(id: string, props: InputProps<Cloudflare.WorkerProps<any, Cloudflare.WorkerAssetsConfig | undefined>, never> | Effect.Effect<InputProps<Cloudflare.WorkerProps<any, Cloudflare.WorkerAssetsConfig | undefined>, never>, never, never>, impl: Effect.Effect<Cloudflare.WorkerShape, ConfigError, Cloudflare.WorkerServices | PlatformServices>) => Effect.Effect<...> (+3 overloads)
Worker
("Worker",
{
main?: Input<string | undefined>

Path to the Worker's entry module. Bundled with rolldown before upload. Mutually exclusive with

script

— provide exactly one.

main
: import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.filename: string

Alias of import.meta.path. Exists for Node.js compatibility

filename
},
import Effect
Effect
.
const gen: <Effect.Effect<Cloudflare.R2BucketClient, never, Cloudflare.R2BucketBinding>, {
fetch: Effect.Effect<HttpServerResponse.HttpServerResponse, Cloudflare.R2Error, HttpServerRequest | RuntimeContext>;
}>(f: () => Generator<Effect.Effect<Cloudflare.R2BucketClient, never, Cloudflare.R2BucketBinding>, {
fetch: Effect.Effect<HttpServerResponse.HttpServerResponse, Cloudflare.R2Error, HttpServerRequest | RuntimeContext>;
}, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
const
const bucket: Cloudflare.R2BucketClient
bucket
= yield*
import Cloudflare
Cloudflare
.
const R2Bucket: ResourceClassWithMethods<Cloudflare.R2Bucket, {
readonly bind: (<Req = never>(args_0: Input<Cloudflare.R2Bucket> | Effect.Effect<Cloudflare.R2Bucket, never, Req>) => Effect.Effect<Cloudflare.R2BucketClient, never, Cloudflare.R2BucketBinding | Req>) & ((bucket: Cloudflare.R2Bucket) => Effect.Effect<any, any, any>);
}>

A Cloudflare R2 object storage bucket with S3-compatible API.

R2 provides zero-egress-fee object storage. Create a bucket as a resource, then bind it to a Worker to read and write objects at runtime.

@sectionCreating a Bucket

@example

Basic R2 bucket

const bucket = yield* Cloudflare.R2Bucket("MyBucket");

@example

Bucket with location hint

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
locationHint: "wnam",
});

@sectionBinding to a Worker

@example

Reading and writing objects

const bucket = yield* Cloudflare.R2Bucket.bind(MyBucket);
// Write an object
yield* bucket.put("hello.txt", "Hello, World!");
// Read an object
const object = yield* bucket.get("hello.txt");
if (object) {
const text = yield* object.text();
}

@example

Streaming upload with content length

const bucket = yield* Cloudflare.R2Bucket.bind(MyBucket);
yield* bucket.put("upload.bin", request.stream, {
contentLength: Number(request.headers["content-length"] ?? 0),
});

@section

Custom Domains

Attach one or more custom domains to serve bucket objects from a hostname you control. The domain's zone must already exist in your Cloudflare account; the zone is inferred from the hostname when omitted, or you can pass a Cloudflare.Zone resource, a zone ID, or any hostname inside the zone via the zone field.

@example

Single custom domain

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
domains: [{ name: "assets.example.com" }],
});

@example

Multiple custom domains

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
domains: [
{ name: "assets.example.com" },
{ name: "static.example.com" },
],
});

@example

Disable a custom domain without removing it

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
domains: [{ name: "assets.example.com", enabled: false }],
});

@example

Custom domain with explicit zone and TLS settings

const zone = yield* Cloudflare.Zone("ExampleZone", {
name: "example.com",
});
const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
domains: [
{
name: "assets.example.com",
zone,
minTLS: "1.2",
},
],
});

@section

Object Lifecycle Rules

Configure lifecycle rules to automatically delete objects, abort incomplete multipart uploads, or transition objects to InfrequentAccess storage. Pass an empty array (or omit) to clear all rules. See the Cloudflare R2 docs for details and limits (max 1000 rules per bucket).

@example

Delete objects 30 days after upload

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
lifecycleRules: [
{
id: "expire-old-objects",
deleteObjectsTransition: {
condition: { type: "Age", maxAge: 60 * 60 * 24 * 30 },
},
},
],
});

@example

Transition to InfrequentAccess after 60 days, delete after 365

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
lifecycleRules: [
{
id: "archive-then-delete",
prefix: "logs/",
storageClassTransitions: [
{
condition: { type: "Age", maxAge: 60 * 60 * 24 * 60 },
storageClass: "InfrequentAccess",
},
],
deleteObjectsTransition: {
condition: { type: "Age", maxAge: 60 * 60 * 24 * 365 },
},
},
],
});

@example

Abort incomplete multipart uploads after 7 days

const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
lifecycleRules: [
{
id: "abort-stale-uploads",
abortMultipartUploadsTransition: {
condition: { type: "Age", maxAge: 60 * 60 * 24 * 7 },
},
},
],
});

R2Bucket
.
bind: <never>(args_0: Input<Cloudflare.R2Bucket> | Effect.Effect<Cloudflare.R2Bucket, never, never>) => Effect.Effect<Cloudflare.R2BucketClient, never, Cloudflare.R2BucketBinding> (+1 overload)
bind
(
import Bucket
Bucket
);
return {
fetch: Effect.Effect<HttpServerResponse.HttpServerResponse, Cloudflare.R2Error, HttpServerRequest | RuntimeContext>
fetch
:
import Effect
Effect
.
const gen: <Effect.Effect<HttpServerRequest, never, HttpServerRequest> | Effect.Effect<Cloudflare.R2ObjectBody | null, Cloudflare.R2Error, RuntimeContext> | Effect.Effect<string, Cloudflare.R2Error, never>, HttpServerResponse.HttpServerResponse>(f: () => Generator<Effect.Effect<HttpServerRequest, never, HttpServerRequest> | Effect.Effect<Cloudflare.R2ObjectBody | null, Cloudflare.R2Error, RuntimeContext> | Effect.Effect<...>, HttpServerResponse.HttpServerResponse, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
const
const request: HttpServerRequest
request
= yield*
const HttpServerRequest: Service<HttpServerRequest, HttpServerRequest>

Server-side representation of an incoming HTTP request.

Details

It extends HttpIncomingMessage with request metadata, parsed cookies, multipart accessors, WebSocket upgrade support, and a modify method for creating adjusted request views.

Service tag for the active server-side HTTP request.

When to use

Use to access the request currently being handled by HTTP server routes and middleware.

@categorymodels

@since4.0.0

@categorycontext

@since4.0.0

HttpServerRequest
;
const
const key: string
key
=
const request: HttpServerRequest
request
.
HttpServerRequest.url: string
url
.
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)

Split a string into substrings using the specified separator and return them as an array.

@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.

@paramlimit A value used to limit the number of elements returned in the array.

split
("/").
Array<string>.pop(): string | undefined

Removes the last element from an array and returns it. If the array is empty, undefined is returned and the array is not modified.

pop
()!;
const
const object: Cloudflare.R2ObjectBody | null
object
= yield*
const bucket: Cloudflare.R2BucketClient
bucket
.
R2BucketClient.get(key: string, options?: Cloudflare.R2GetOptions): Effect.Effect<Cloudflare.R2ObjectBody | null, Cloudflare.R2Error, RuntimeContext> (+1 overload)
get
(
const key: string
key
);
return
const object: Cloudflare.R2ObjectBody | null
object
?
import HttpServerResponse
HttpServerResponse
.
const text: (body: string, options?: HttpServerResponse.Options.WithContentType) => HttpServerResponse.HttpServerResponse

Creates an HTTP response whose body is a string.

@categoryconstructors

@since4.0.0

text
(yield*
const object: Cloudflare.R2ObjectBody
object
.
R2ObjectBody.text(): Effect.Effect<string, Cloudflare.R2Error>
text
())
:
import HttpServerResponse
HttpServerResponse
.
const text: (body: string, options?: HttpServerResponse.Options.WithContentType) => HttpServerResponse.HttpServerResponse

Creates an HTTP response whose body is a string.

@categoryconstructors

@since4.0.0

text
("Not found", {
status?: number | undefined
status
: 404 });
}),
};
}),
);

The Worker resource declaration moves from alchemy.run.ts into the Worker file itself (using import.meta.filename as the main), and the Stack just yield*-s the imported Worker:

alchemy.run.ts
import * as
import Alchemy
Alchemy
from "alchemy";
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Effect
Effect
from "effect/Effect";
import
import Worker
var Worker: Effect.Effect<Cloudflare.Worker<{
readonly Bucket: any;
}>, never, Cloudflare.Providers>
Worker
from "./src/worker.ts";
import {
import Bucket
Bucket
} from "./src/bucket.ts";
export type
type WorkerEnv = {
readonly Bucket: any;
}
WorkerEnv
=
import Cloudflare
Cloudflare
.
type InferEnv<W> = W extends Effect.Effect<infer A, infer _E, infer _R> ? Cloudflare.InferEnv<A> : W extends Cloudflare.Worker<any> ? Cloudflare.InferEnv<Exclude<W["Props"]["env"], undefined>> : { [k in keyof W]: Cloudflare.GetBindingType<W[k]>; }
InferEnv
<typeof
import Worker
var Worker: Effect.Effect<Cloudflare.Worker<{
readonly Bucket: any;
}>, never, Cloudflare.Providers>
Worker
>;
export const
const Worker: Effect.Effect<Cloudflare.Worker<{
readonly Bucket: any;
}>, never, Cloudflare.Providers>
Worker
=
import Cloudflare
Cloudflare
.
const Worker: <{
readonly Bucket: any;
}, undefined, never>(id: string, props: Alchemy.InputProps<Cloudflare.WorkerProps<{
readonly Bucket: any;
}, undefined>, never> | Effect.Effect<Alchemy.InputProps<Cloudflare.WorkerProps<{
readonly Bucket: any;
}, undefined>, never>, ConfigError, never>) => Effect.Effect<Cloudflare.Worker<{
readonly Bucket: any;
}>, never, Cloudflare.Providers> (+3 overloads)
Worker
("Worker", {
main?: Alchemy.Input<string | undefined>

Path to the Worker's entry module. Bundled with rolldown before upload. Mutually exclusive with

script

— provide exactly one.

main
: "./src/worker.ts",
env?: Alchemy.Input<{
readonly Bucket: any;
} | undefined>

Environment variables and native Cloudflare Bindings to bind to the Worker. Accepts:

  • Resource references (R2 bucket, KV namespace, D1 database, another Worker, Durable Object, etc.) — emitted as the corresponding native binding.
  • effect/Config values (Config.redacted, Config.string, Config.number, …) — resolved at deploy time and bound as secret_text on Cloudflare regardless of the Config constructor used. See

https://v2.alchemy.run/concepts/secrets Concepts › Secrets and Variables

.

  • Literal values — routed by shape: Redacted<string>secret_text, stringplain_text, anything else → json.

In Effect-native Workers you can alternatively yield* a Config in the Init phase to register the binding implicitly; env is the only option for async (non-Effect) Workers.

env
: {
type Bucket: any
Bucket
},
});
export default
import Alchemy
Alchemy
.
Stack<{
url: Alchemy.Output<string | undefined, never>;
}, unknown>(stackName: string, options: Alchemy.StackProps<unknown>, eff: Effect.Effect<{
url: Alchemy.Output<string | undefined, never>;
}, ConfigError, unknown>): Effect.Effect<Alchemy.CompiledStack<{
url: Alchemy.Output<string | undefined, never>;
}, any>, ConfigError, never> (+2 overloads)
export Stack
Stack
(
"MyApp",
{
StackProps<unknown>.providers: Layer<unknown, never, Alchemy.StackServices>
providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Profile | CredentialsStore | Alchemy.Provider<Command> | Alchemy.Provider<Alchemy.KeyPair> | Alchemy.Provider<Alchemy.Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, Alchemy.Stack | Alchemy.Stage | Scope | FileSystem | Path | Alchemy.AlchemyContext | HttpClient | ChildProcessSpawner | Alchemy.AuthProviders>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
() },
import Effect
Effect
.
const gen: <any, {
url: Alchemy.Output<string | undefined, never>;
}>(f: () => Generator<any, {
url: Alchemy.Output<string | undefined, never>;
}, never>) => Effect.Effect<{
url: Alchemy.Output<string | undefined, never>;
}, unknown, unknown> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
const
const bucket: any
bucket
= yield*
import Bucket
Bucket
;
const
const worker: Cloudflare.Worker<{
readonly Bucket: any;
}>
worker
= yield*
import Worker
var Worker: Effect.Effect<Cloudflare.Worker<{
readonly Bucket: any;
}>, never, Cloudflare.Providers>
Worker
;
return {
url: Alchemy.Output<string | undefined, never>
url
:
const worker: Cloudflare.Worker<{
readonly Bucket: any;
}>
worker
.
url: Alchemy.Output<string | undefined, never>
url
};
}),
);
v1 (async)v2 (async style)v2 (Effect style)
Stackawait alchemy("name")Alchemy.Stack("name", ...)Alchemy.Stack("name", ...)
Resourcesawait R2Bucket(...)Cloudflare.R2Bucket(...)Cloudflare.R2Bucket(...)
Worker entryentrypointmainmain: import.meta.filename
Bindingsbindings: { KEY: resource }env: { Key: resource }yield* Resource.bind(ref)
Runtime codeasync fetch(req, env)async fetch(req, env)Effect.gen(function* () { ... })
Lifecycleawait app.finalize()automaticautomatic
Type safetyruntime errorstyped env via InferEnvfull Effect type system