Everything a Cloudflare Worker can use — KV, R2, D1, Hyperdrive, queues, the cache, rate limits, Durable Objects, WebSockets, cron — is an effect a program names; each cloud is a set of handlers. The same program runs on Cloudflare Workers, on AWS Lambda, and in memory for tests. State is kept without replay.
A serverless instance is evicted, restarted, or run twice at once. Temporal and Azure Durable Functions answer with replay: a journal, the code run again, and code that must not change while a run lives. Here what is kept is the state itself, written atomically with each step — so a new version of the code goes on from the old state.
Durable.step reads a snapshot and its own writes, keeps its writes and commits them all at once with its alarm; nothing written if it fails; run again if what it read changed meanwhile.
Durable.machine(name, init)(step): the state loaded, the step run, the new state committed. Durable.once: an external call once per step, with a stable idempotency key. Durable.wakeAt: an alarm, set only if the step commits.
An account as a durable process, in a Durable Object (from the example Worker, run end to end on wrangler dev):
def event(name: String, e: Json): Account ! A =
Durable.machine[Account, Json, AD, A]("account", Account(0, Nil)) { (a, e) =>
(e.field("op"), e.field("amount")) match
case (Some(Json.Str("deposit")), Some(Json.Num(n))) => pure(a.copy(balance = a.balance + n.toLong))
case (Some(Json.Str("pay")), Some(Json.Num(n))) =>
if n.toLong > a.balance then Fail.raise[String, AD](s"${n.toLong} is more than the balance ${a.balance}")
else Durable.once[String, AD]("payment")(key => pure(s"pay-$key")).map(id =>
a.copy(balance = a.balance - n.toLong, payments = a.payments :+ id))
case (Some(Json.Str("interest-in")), Some(Json.Num(ms))) =>
Clock.now[AD].flatMap(t => Durable.wakeAt[AD](t + ms.toLong)).as(a)
case (Some(Json.Str("interest")), _) => pure(a.copy(balance = a.balance + a.balance / 100))
case _ => Fail.raise[String, AD](s"an event not known: ${Json.print(e)}")
}(e)
Platforms of the state: a Durable Object's SQLite (one transaction a commit), DynamoDB (a conditional TransactWriteItems; two instances at once lose nothing), memory with faults (a crash before or after a commit, a conflicting writer, an eviction) — one set of scenarios every platform passes. A twin on okay-simple, for Scala 2.13 and 3, keeps the same stored format.
Each effect is the program's; each cloud is a Platform of ports, one an effect. A resource is a logical name the platform maps to its binding. What a cloud lacks, its port says by name.
| Effect | Cloudflare | AWS | Memory |
|---|---|---|---|
Kv | KV | DynamoDB (TTL) | memory |
Blobs | R2 | S3 — not yet | memory |
Sql | D1; Postgres through Hyperdrive | Postgres (RDS Proxy) | SQLite file (JVM) |
Queues | Queues, a consumer | SQS events in; sending — not yet | memory, dead letters |
Fetch | fetch | fetch (Node) | a function |
Cache | Cache API | — | memory |
Env | vars, secrets | process environment | maps |
Services | service bindings | Lambda invoke — not yet | apps by name |
Limits | Rate Limiting | a DynamoDB counter | fixed window |
Ai | Workers AI (written, not run locally) | Bedrock — not yet | a function |
Vectors | Vectorize (written, not run locally) | not yet | cosine |
Analytics | Analytics Engine | CloudWatch metrics (EMF) | kept |
Mails | send_email | SES — not yet | kept |
Objects | Durable Objects | the same Lambda, its state a DynamoDB space | in process |
Sockets | WebSockets (hibernation) | not yet | in memory |
HTTP is okay-http's model, a body text or bytes; JSON is okay-simple's; Env, Log, Clock, Async are okay-scala's own. A Worker is an App (fetch, scheduled, queue, mail), an object class an Actor (fetch, alarm, socket messages); a Lambda takes API Gateway's, SQS's and EventBridge's events. Excerpts of the example Worker:
override def fetch(r: Request): Response ! Cloud = r.path match
case "/" => Objects.call[Cloud]("ACCOUNTS", r.param("account").getOrElse("main"), r)
case "/queue" => Queues.send[Cloud]("JOBS", q(r, "body")).as(Response.text("queued"))
case "/cached" => Cache.through[Cloud]("https://okay.example/cached", 60)(
Clock.now[Cloud].map(t => Response.text(s"made at $t")))
case "/limit" => Limits.take[Cloud]("LIMITER", q(r, "key")).map(ok => Response.text(ok.toString))
case _ => pure(Response.notFound())
override def queue(queue: String, m: Message): Settle ! Cloud =
Kv.put[Cloud]("KV", s"job:${m.body}", s"done after ${m.attempts}").andThen(Log.info[Cloud](s"job ${m.body}"))
.as(Settle.Ack)
Cloudflare Workflows replay a function. Here a workflow is a list of stages — a step retried with backoff, a sleep, a wait for an event — run by an object: each stage one durable step, a sleep an alarm, an event a request. Restarted anywhere, it goes on from the stage committed.
val Orders: Workflow[Json] = Workflow[Json](identity)(
Stage.Do("charge", o => Kv.put[InActor + Fail % String]("KV",
s"charged:${o.field("id").fold("?")(j => Json.print(j))}", "yes").as(o)),
Stage.Sleep("pause", 1000),
Stage.Wait("approval", "approved", (o, j) => o match
case Json.Obj(fs) => Json.Obj(fs :+ ("approvedBy" -> j.field("by").getOrElse(Json.Null)))
case other => other),
Stage.Do("mail", o => Mails.send[InActor + Fail % String]("MAIL", Mail("shop@okay.example", "ann@okay.example",
"approved", Json.print(o))).as(o)))
okay-store's effects are data, so a cloud program performs them as they are: a type's repository (Repo.save, find, create…) and documents with a query (Docs), handled on SQL (D1, Postgres through Hyperdrive or RDS Proxy, SQLite), on KV, or in an object's own durable state — a store per tenant, a step one transaction. The SQL is okay-store's own.
final case class Sale(id: String, item: String, qty: Int)
val sales: Mapping[Sale] = Mapping.keyed("sales", Shape.derived[Sale])(_.id)
type Sales[X] = Repo.Of[Sale][X]
def selling[R[_]](item: String, qty: Int)(using In[Sales, R]): String ! R =
Repo.create[Sale, R](k => Sale(k, item, qty)).flatMap(s =>
Repo.find[Sale, R](sales.at(_.qty) > 1, List(sales.at(_.qty).desc)).flatMap(big =>
Repo.count[Sale, R]().map(n => s"${s.id} of $n; big: ${big.map(_.item).mkString(",")}")))
type CD[X] = (Cloud + Data)[X]
type CS[X] = (CD + Sales)[X]
def sold(r: Request, db: String, dialect: Dialect): Response ! Cloud =
StoreOn.sql[CD, Cloud, String](db, dialect)(StoreOn.data[Sale, CS, CD, String](sales)(
selling[CS](r.param("item").getOrElse("?"), r.param("qty").map(_.toInt).getOrElse(1)))).map(Response.text(_))
okay-store's journaled records (Store: kinds, versions, the journal, the audit) run here by okay-store's own handler, put into the cloud program by okay-simple's bridge (OnCek.into) — one machine, one run, and in an object one transaction with the rest of the step. Checked against okay-store's own reference on memory, SQLite, Postgres and wrangler dev.
The same mapped type, served by okay-faces' own REST resources and MCP tools — described, not written — its operations on D1:
/** THE TYPE SERVED: okay-faces' REST resources and MCP tools of `sales`, from its description alone */
val salesRest = Faces.rest(Mapping.describe(sales), "/api/sales")
val salesMcp = Faces.mcp(Mapping.describe(sales), "shop")
/** the face's program in the cloud's row, the type on D1 */
def served(face: Request => OkayCek[Response], r: Request): Response ! Cloud =
StoreOn.sql[CD, Cloud, Response]("DB", Dialect.sqlite)(StoreOn.data[Sale, CS, CD, Response](sales)(
OnCek.into[CS](face(r))))
type OkayCek[A] = OnCek.![A, Faces.Only[Repo.Of[Sale]]]
On wrangler dev: the description, a create with a key from D1, a search by a condition; MCP's initialize, the tools and a call on the same D1; a shop's REST on its own Durable Object. A Lambda serves it from Postgres the same way.
Released: 0.1.0, a Maven repository on GitHub Pages (how to add it). Scala 3.3 LTS; Scala.js 1.22 or newer.
wrangler dev (every binding but Workers AI and Vectorize, which have no local mode); AWS on DynamoDB Local and a Postgres.Auth.guarded takes a bearer token to its caller (a secret, tokens in KV, any check) — on Workers, Lambdas and in memory; an object's event stream behind it, checked on wrangler dev.