Skip to content

First-party `bynk` capabilities

Bynk ships a small set of capabilities with the compiler, under the reserved bynk namespace. You consume them like any capability — consumes the unit, then given the capability in the handlers that need it — but you never declare or provide them: the toolchain injects the implementation for the target platform. The bynk root namespace is reserved, so your own code can never collide with it.

There are two units:

UnitConsume withPortability
bynkconsumes bynk { … }Portable — the same source runs on the cloudflare and node platforms.
bynk.cloudflareconsumes bynk.cloudflare { … }Platform-locked — consuming it pins the deployment unit to Cloudflare.

For why a capability is the unit of outside-world access, see Understand the capability model; for the general capability / provides / given rules, see Capabilities & providers.

consumes bynk { … } brings these into scope. They are implemented identically on every platform (the host Date.now, crypto, fetch, console, environment), so code that stays on this surface is portable.

CapabilityOperations
Clocknow() -> Effect[Instant] — the current instant (an absolute point in time; epoch-millis under the hood).
Randomuuid() -> Effect[Uuid] · int(lo: Int, hi: Int) -> Effect[Int] (lo-inclusive, hi-exclusive).
Loggerinfo(msg: String) -> Effect[()] · error(msg: String) -> Effect[()].
Fetchsend(req: Request) -> Effect[Result[Response, FetchError]] — an outbound HTTP request.
Secretsget(name: String) -> Effect[Option[String]] — read configuration/secrets; None if unset.
Localecurrent() -> Effect[LocaleTag] — the locale to render messages in. On Cloudflare, negotiated from the request’s Accept-Language against a context’s message bundle; a fixed "en" on every other platform, and on Cloudflare without a detectable bundle. See Understand localisation.
Idempotencydedup[T](key: String) -> Effect[Option[T]] · remember[T](key: String, value: T, expiresAfter: Duration) -> Effect[()] — mechanical dedup for at-least-once delivery. See below.
Eventsemit[E](event: E) -> Effect[()] — emit an event declared by the calling context, fire-and-forget. See below.

Idempotency makes at-least-once delivery (a retried command, a replayed event) safe: check dedup for a cached outcome, and on a miss, cache the value you compute with remember so a later call with the same key gets it back instead of recomputing:

on reserve(orderId: OrderId) -> ReserveOutcome given Idempotency {
let cached <- Idempotency.dedup[ReserveOutcome](Json.encode(orderId))
match cached {
Some(outcome) => outcome,
None => {
let outcome = ... compute the real outcome ...
let _ <- Idempotency.remember[ReserveOutcome](Json.encode(orderId), outcome, 24.hours)
outcome
}
}
}

Both operations take their type argument explicitly (dedup[ReserveOutcome], remember[ReserveOutcome]) — always, even where an argument’s type would otherwise make it obvious, since a capability operation’s own type parameter is never inferred (see Capabilities & providers). Calling dedup alone does not cache anything — remember is what writes the entry, so forgetting to call it after a cache miss means that key is recomputed every time, not an error. The shipped provider is a single in-memory map, identical on every platform: state is lost on process restart, and there is no durability or multi-instance sharing yet — a durable, platform-native provider is a named but unfiled future direction (see the track’s own settling notes for why that’s a separate axis from portability, not a variant of it).

The key you pass is automatically scoped to the calling handler’s own qualified name before it reaches the provider — Idempotency.dedup[T]("order-1") inside on call of service ordering in context shop.reserve actually stores against "shop.reserve.ordering.call::order-1". Two unrelated handlers can therefore never collide on the same literal key by accident; only two calls that are genuinely the same call site (including the same call reached via a cross-context alias) ever share a scope. This doesn’t stop two different callers of the same handler from colliding if the key you supply doesn’t itself distinguish them (two tenants both calling reserve with the same order ID, say) — differentiating those is still on you, the same discipline any caller-supplied idempotency key requires.

Events emits an event — a typed fact a context declares — for any number of other contexts to subscribe to with from Events(E):

context commerce.order
exports transparent { PaymentConfirmed }
event PaymentConfirmed = {
orderId: String,
}
service markPaid {
on call(orderId: String) -> Effect[()] given Events {
Events.emit[PaymentConfirmed](PaymentConfirmed { orderId: orderId })
}
}

emit’s type argument is explicit, like Idempotency’s operations above — never inferred from the value’s type. Only the context that declares an event may emit it, even though the type is visible cross-context for subscription (bynk.event.emit_outside_owner otherwise); emission is released only if the emitting handler itself commits, so an aborted handler emits nothing. See Understand events for the full model, the subscriber side (from Events(E), on event), and how delivery differs between Cloudflare and Bundle targets.

The bynk unit also exports the transparent types these operations use:

type Uuid = String where Matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
type Method = enum { Get, Post, Put, Delete }
type FetchError = enum { Network, Timeout }
type Request = {
method: Method,
url: String,
contentType: Option[String],
authorization: Option[String],
body: Option[String],
}
type Response = {
status: Int,
body: String,
}
type EventEnvelope = {
eventId: String,
publisherId: String,
emittedAt: Instant,
schemaVersion: Int,
}

EventEnvelope is an on event handler’s optional second parameter — see Understand events.

The Cloudflare surface — bynk.cloudflare

Section titled “The Cloudflare surface — bynk.cloudflare”

consumes bynk.cloudflare { … } exposes the platform’s own infrastructure. Consuming it locks the deployment unit to Cloudflare (it cannot then target node), and the generated wrangler.toml gains the matching binding stanza.

CapabilityOperations
Kvget(key: String) -> Effect[Option[String]] · put(key: String, value: String) -> Effect[()] · putTtl(key: String, value: String, ttlSeconds: Int) -> Effect[()] · delete(key: String) -> Effect[()] · list(prefix: Option[String]) -> Effect[List[String]].

Kv is backed by a single Worker KV namespace bound as env.KV; the [[kv_namespaces]] stanza is derived for you.

Noteproducing queue messages (send/sendBatch) is not yet a first-party capability. Consuming a queue is a separate entry-point feature: see Process a queued message and the Queue reference.

Consume the unit, then grant the capability with given in each handler that calls it — exactly as you would a capability you declared yourself:

context greeter
consumes bynk { Clock, Logger }
service api from http {
on GET("/now") () -> Effect[HttpResult[Int]] by Visitor given Clock, Logger {
let t <- Clock.now()
let _ <- Logger.info("checked the clock")
Ok(t.toEpochMillis())
}
}

The Cloudflare surface is consumed the same way — consumes bynk.cloudflare { Kv } and given Kv — with the portability trade-off noted above. To configure a first-party capability (an API key for Fetch, say), read it from Secrets rather than passing it as an argument; Wrap a library as an adapter shows the pattern.

Beyond capabilities, the bynk namespace also ships pure commons you bring in with uses (not consumes) — bynk.list and bynk.map (combinators over the List/Map kernels) and bynk.string (string helpers). These are ordinary functions with no effects; see the type system reference.

Two more serve the Locale capability above. bynk.locale.types is a dependency-free leaf declaring LocaleTag, Message and MessageArguses it wherever you only need to name those types, which is all a context calling Locale.current() requires. bynk.locale declares the value-level API: the message/withText/ withWhole/withNum/withMoment builders and the bundle-free render fallback. A commons declaring a messages bundle needs both; a context consuming that bundle should uses only bynk.locale.types, since bynk.locale and every message-bundle commons both export a render. See Localisation.

Deprecated (v0.91, ADR 0116 D6): the bynk.list free functions whose method forms now exist — map, filter, find, any, all — emit a non-failing bynk.list.deprecated_function warning at each call site (the build still succeeds) with a machine-applicable fix to the method form: map(xs, f)xs.map(f), find(xs, p)xs.filter(p).first(), and so on. Prefer the List methods. reverse and traverse keep their free-function form (no method equivalent yet).

See also: Capabilities & providers, Adapters, Understand the capability model.