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
Section titled “Cloudflare.Ruleset”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.
Durable Object chat persistence
Section titled “Durable Object chat persistence”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.
effect/Config in stack bodies
Section titled “effect/Config in stack bodies”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), }),);Also in this release
Section titled “Also in this release”- 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.Secretworks in a Worker’senv— the provider maps it to a nativesecrets_store_secretbinding, so the runtime sees a realSecretsStoreSecretwith.get()(#475).- Drizzle
Schemapreserves snapshot lineage — the previous snapshot id is passed togenerateDrizzleJsonwhen generating migrations. Thanks Julius Marminge (#519). - Outputs are identified by symbol, not a duck-typed
kindproperty — user data containing akindfield is no longer misclassified as an Output expression. Thanks Julius Marminge (#517). - PlanetScale MySQL/Postgres Database outputs drop the
kindattribute that collided with the output proxy discriminator (#518). - Website: the theme toggle no longer collides with the mobile hamburger menu (#499).