Skip to content

2.0.0-beta.51 - Rulesets & Chat Persistence

v2.0.0-beta.51 brings a new Cloudflare.Ruleset resource, a Durable Object-backed persistence layer for Effect AI chats, an S3 → Lambda notification fix, and ConfigError in the error channel of Alchemy.Stack and Worker effects.

Cloudflare.Ruleset owns a zone’s ruleset phase entrypoint — WAF custom rules, redirects, or any of Cloudflare’s ruleset phases. Pass a Zone, a phase, and the rules; the resource reconciles the entire phase entrypoint. Thanks Alex (#240).

const zone = yield* Cloudflare.Zone("MyZone", { name: "example.com" });
const waf = yield* Cloudflare.Ruleset("WafRules", {
zone,
phase: "http_request_firewall_custom",
rules: [
{
description: "Block exploit probes",
expression: `lower(http.request.uri.path) contains "/.env"`,
action: "block",
},
],
});

Outputs include rulesetId, kind, version, and the resolved rules. Changing the zone or phase replaces the ruleset; rule edits are in-place updates. Because the resource owns the whole phase entrypoint, rules managed elsewhere in the same phase are overwritten on deploy.

Cloudflare.DurableObjectChatPersistence is a BackingPersistence layer (Effect AI’s persistence module) backed by the surrounding Durable Object’s state.storage. Drop it under Chat.makePersisted({ storeId }) and chat history lives in the DO’s SQLite store — surviving hibernation and eviction — with ${storeId}: key namespacing so multiple stores coexist in one DO (#390).

One DO instance == one persisted conversation thread:

import { Chat } from "effect/unstable/ai";
export default class ChatBackend extends Cloudflare.DurableObjectNamespace<ChatBackend>()(
"ChatBackend",
Effect.gen(function* () {
// Init: bind the AI Gateway and build the LanguageModel layer.
const aiGateway = yield* Cloudflare.AiGateway.bind(Gateway);
const languageModel = aiGateway.model({
model: "@cf/meta/llama-3.1-8b-instruct",
});
return Effect.gen(function* () {
// Per-instance: chat history backed by this DO's storage.
const persistence = yield* Chat.makePersisted({
storeId: "alchemy.chat",
}).pipe(Effect.provide(Cloudflare.DurableObjectChatPersistence));
return {
send: (threadId: string, prompt: string) =>
Effect.gen(function* () {
const chat = yield* persistence.getOrCreate(threadId);
const response = yield* chat.generateText({ prompt });
return response.text;
}).pipe(Effect.provide(languageModel), Effect.orDie),
};
});
}).pipe(Effect.provide(Cloudflare.AiGatewayBindingLive)),
) {}

Paired with AI Gateway, that’s a fully persisted per-thread chat backend. One caveat: TTL is currently ignored — DO storage has no native TTL.

S3 event notifications actually reach Lambda

Section titled “S3 event notifications actually reach Lambda”

S3 bucket notifications subscribed via S3.notifications(bucket).subscribe(...) on a Lambda event source were never delivered: the notification configuration was recorded in state but never applied to the bucket, and the auto-created Lambda permission used the invalid action string lambda.InvokeFunction instead of lambda:InvokeFunction. Both are fixed, and the notification props gained prefix / suffix key filters (mapped to S3 FilterRules). Thanks Andrey Konopkov (#470).

yield* S3.notifications(bucket, {
events: ["s3:ObjectCreated:*"],
prefix: "incoming/",
}).subscribe((stream) =>
stream.pipe(
Stream.runForEach((event) =>
putObject({
Key: `processed/${event.key.slice("incoming/".length)}`,
Body: JSON.stringify({ key: event.key, size: event.size }),
ContentType: "application/json",
}).pipe(Effect.orDie),
),
),
);

The prefix filter matters when the handler writes back into the same bucket — without it, the processed/ write would itself trigger the subscription, which writes another object, and so on.

Alchemy.Stack, Cloudflare.Worker, and the other Platform effects now permit ConfigError in their error channel instead of requiring never. Reading configuration with Config.string in a stack body previously forced an Effect.orDie wrapper; now it type-checks directly and the ConfigError surfaces on the resulting effect (#487).

const stack = Alchemy.Stack(
"my-app",
{ providers, state },
Effect.gen(function* () {
const value = yield* Config.string("SOME_CONFIG");
return { value };
}).pipe(Effect.orDie),
}),
);
  • Resource-scoped adopt(...) is honored by the planner.pipe(adopt(true)) on a resource overrides the stack/CLI-wide adopt default, in both directions (#521).
  • Cloudflare.SecretsStore.Secret works in a Worker’s env — the provider maps it to a native secrets_store_secret binding, so the runtime sees a real SecretsStoreSecret with .get() (#475).
  • Drizzle Schema preserves snapshot lineage — the previous snapshot id is passed to generateDrizzleJson when generating migrations. Thanks Julius Marminge (#519).
  • Outputs are identified by symbol, not a duck-typed kind property — user data containing a kind field is no longer misclassified as an Output expression. Thanks Julius Marminge (#517).
  • PlanetScale MySQL/Postgres Database outputs drop the kind attribute that collided with the output proxy discriminator (#518).
  • Website: the theme toggle no longer collides with the mobile hamburger menu (#499).