Skip to content

2.0.0-beta.63 - Durable Object Transfers & Action Bindings

beta.63 moves Durable Object classes between Workers with their data intact, lets Actions bind resources and read Outputs with your CLI credentials, and makes alchemy login work from any folder. The website now lives at alchemy.run.

A Durable Object class can now move between Workers with its data intact — same instances, same storage — via Cloudflare’s transferred_classes migration (#803).

Moves are always declared. A class that disappears from one Worker and appears on another is ambiguous between “transfer the data” and “delete it, start fresh”, so Alchemy never guesses. The class states where it came from:

export class Counter extends Cloudflare.DurableObject<
Counter,
{
increment: () => Effect.Effect<number, never, RuntimeContext>;
get: () => Effect.Effect<number, never, RuntimeContext>;
}
>()("Counter") {}
>()("Counter", { transferredFrom: "WorkerA" }) {}

Whichever Worker hosts Counter next transfers the namespace from WorkerA. transferredFrom accepts a Worker logical id, a physical script name (for cross-stack moves), the Worker class itself, or a thunk for import cycles.

The declaration is a host history: after a second move you write transferredFrom: ["WorkerA", "WorkerB"], and the namespace transfers from whichever listed host currently holds it — so a stage that lagged behind or skipped a release still converges. Once a transfer completes, the declaration is inert and safe to keep in code forever.

If you move a host without declaring it, the deploy fails before any upload with the typed DurableObjectTransferRequired error, naming the exact declaration to add. Durable Object data is never silently destroyed or forked.

Docs: Bind to another Worker’s Durable Object.

Actions run arbitrary Effects during deploy — seed a database, ping Slack, invalidate a cache. They can now bind resources the same way a Worker does, and read resource Outputs (#807):

const Seed = Alchemy.Action(
"Seed",
Effect.gen(function* () {
const db = yield* Cloudflare.D1.QueryDatabase(database);
const databaseId = yield* database.databaseId;
return Effect.fn(function* () {
yield* db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)");
return { databaseId: yield* databaseId };
});
}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseLocal)),
);

The new *Local layers make this work. They’re a third binding variant alongside *Binding (native Worker binding) and *Http (scoped API token): a *Local layer authenticates with your current CLI credentials and returns the same typed client, so an Action running on your machine or in CI talks to the same data plane your Worker would. Every Cloudflare capability with an HTTP data plane has one — D1, KV, R2, Queues, DNS, Vectorize, Tunnel, AI Search, Browser Rendering, and more.

Reading yield* database.databaseId inside an Action also records a dependency edge, so the Action waits for the database to exist before it runs.

alchemy login no longer requires an alchemy.run.ts (#842). With no entrypoint in sight, it offers every built-in auth provider and configures the ones you pick, so you can set up or refresh credentials from any folder:

$ cd /some/empty/dir && alchemy login
No alchemy.run.ts found — using Alchemy's built-in providers.
◆ Select providers to log in with
│ ◻ AWS
│ ◻ Axiom
│ ◻ Cloudflare
│ ◼ GitHub (configured)
│ ◻ Neon
│ ◻ Planetscale

Already-configured providers are pre-selected and labeled. In CI the prompt is skipped and all built-ins are configured non-interactively. With an alchemy.run.ts present, behavior is unchanged.

Docs: alchemy login.

The v2 site now serves from the apex domain, and v2.alchemy.run 301s to it at the edge (#806). The redirect is itself deployed by Alchemy — a Cloudflare.Ruleset on the adopted zone:

yield* Cloudflare.Ruleset.Ruleset("V2Redirect", {
zone,
phase: "http_request_dynamic_redirect",
rules: [{
expression: 'http.host eq "v2.alchemy.run"',
action: "redirect",
actionParameters: {
fromValue: {
targetUrl: { expression: 'concat("https://alchemy.run", http.request.uri.path)' },
preserveQueryString: true,
statusCode: 301,
},
},
}],
});

This release also lands alchemy-test, the Effect-native test runner that executes our 4,000+ live-cloud test suite in a single bun process (#848). It got its own post: A Test Runner for Agents and Humans.

  • Durable Object metadata packs into a single script tag (#824) — one tag per DO binding blew Cloudflare’s 10-tag-per-Worker limit at 7+ DOs; all mappings now fit in one tag.
  • Vite-based Workers hash sibling workspace packages (#822) — editing only a sibling package used to skip the deploy and serve stale code.
  • Alchemy’s Vite plugin no longer double-registers (#833) — apps that also build standalone keep the plugin in their vite.config.ts; Alchemy signals its own injection via ALCHEMY_CLOUDFLARE_VITE_INJECTED. Thanks Alex!
  • MySQL replicas reconcile in place (#825) — changing replicas on a PlanetScale MySQL database resizes the keyspace instead of replacing the database.
  • Unresolved Outputs never persist in state (#835) — an interrupted create could snapshot live Output proxies into state and poison later reads.
  • Auto-bound Config resolves in request handlers (#829) — re-yielding a Config inside a handler decoded the secret marker JSON instead of the value.
  • proxyChain values pass Effect.isEffect (#827) — chained Drizzle clients now work with predicate-dispatched operators like Effect.catchTag. Thanks Julian Archila!