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:
| Unit | Consume with | Portability |
|---|---|---|
bynk | consumes bynk { … } | Portable — the same source runs on the cloudflare and node platforms. |
bynk.cloudflare | consumes 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.
The portable surface — bynk
Section titled “The portable surface — bynk”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.
| Capability | Operations |
|---|---|
Clock | now() -> Effect[Instant] — the current instant (an absolute point in time; epoch-millis under the hood). |
Random | uuid() -> Effect[Uuid] · int(lo: Int, hi: Int) -> Effect[Int] (lo-inclusive, hi-exclusive). |
Logger | info(msg: String) -> Effect[()] · error(msg: String) -> Effect[()]. |
Fetch | send(req: Request) -> Effect[Result[Response, FetchError]] — an outbound HTTP request. |
Secrets | get(name: String) -> Effect[Option[String]] — read configuration/secrets; None if unset. |
Locale | current() -> 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. |
Idempotency | dedup[T](key: String) -> Effect[Option[T]] · remember[T](key: String, value: T, expiresAfter: Duration) -> Effect[()] — mechanical dedup for at-least-once delivery. See below. |
Events | emit[E](event: E) -> Effect[()] — emit an event declared by the calling context, fire-and-forget. See below. |
The Idempotency capability
Section titled “The Idempotency capability”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.
The Events capability
Section titled “The Events capability”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.
| Capability | Operations |
|---|---|
Kv | get(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.
Note — producing 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.
Consuming a first-party capability
Section titled “Consuming a first-party capability”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.
Related first-party modules
Section titled “Related first-party modules”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 MessageArg — uses 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.listfree functions whose method forms now exist —map,filter,find,any,all— emit a non-failingbynk.list.deprecated_functionwarning 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 theListmethods.reverseandtraversekeep their free-function form (no method equivalent yet).
See also: Capabilities & providers, Adapters, Understand the capability model.