Toggle a feature flag
Store feature flags as JSON in Workers KV, let any visitor read them, and let only an authenticated editor create, change, or delete them.
commons keys
---A flag name: 1–64 characters. Used as part of the KV key, so it is validated atthe boundary before any handler runs. A refined `String`, so it projectsstructurally across the context boundary — the HTTP service mixes in this sameconstraint for its path parameters.---type FlagKey = String where MinLength(1) and MaxLength(64)
---The KV key for a flag. Flags share a KV namespace with anything else, so they arekey-prefixed.---fn keyOf(name: FlagKey) -> String { "flag:\(name)"}
---Strip the `flag:` prefix (5 characters) back to the bare name — the inverse of`keyOf`, used when listing.---fn nameOf(key: String) -> String { key.slice(5, key.length())}context flags
uses keysconsumes bynk.cloudflare { Kv } -- locks this unit to Cloudflare
---A user proves identity with a JWT. `Editor` is the same user who additionallycarries an `editor` claim — authentication (401) stays distinct fromauthorisation (403).---type UserId = String where NonEmpty
actor User { auth = Bearer(secret = "AUTH_JWT_SECRET"), identity = UserId }actor Editor = User where hasClaim("editor")
---The stored shape of a flag. Persisted as JSON in KV and decoded on read. (The`FlagKey` type and the `keyOf`/`nameOf` key helpers live in `commons keys`, wherethey are unit-tested.)---type Flag = { enabled: Bool, description: String,}
service api from http { -- Public: list the names of all known flags. on GET("/flags") by Visitor () -> Effect[HttpResult[List[String]]] given Kv { let keys <- Kv.list(Some("flag:")) Ok(keys.map((k) => nameOf(k))) }
-- Public: read one flag. on GET("/flags/:name") by Visitor (name: FlagKey) -> Effect[HttpResult[Flag]] given Kv { let stored <- Kv.get(keyOf(name)) match stored { None => NotFound Some(s) => match Json.decode[Flag](s) { Ok(flag) => Ok(flag) Err(_) => ServerError("stored flag is corrupt") } } }
-- Editors only: create or update a flag. A non-editor token is 403, an -- anonymous request 401 — both enforced at the boundary. on PUT("/flags/:name") by e: Editor (name: FlagKey, body: Flag) -> Effect[HttpResult[Flag]] given Kv { let _ <- Kv.put(keyOf(name), Json.encode(body)) Ok(body) }
-- Editors only: remove a flag. on DELETE("/flags/:name") by e: Editor (name: FlagKey) -> Effect[HttpResult[String]] given Kv { let _ <- Kv.delete(keyOf(name)) NoContent }}This example reaches Workers-only shapes (storage bindings, agents, or cron), so it runs with bynk dev rather than in the browser playground. See Install to get started.
How it works
Section titled “How it works”The flag name is a refined type — FlagKey = String where MinLength(1) and MaxLength(64) — defined in commons keys alongside the key helpers keyOf (which
namespaces a name as flag:…) and nameOf (its inverse, stripping the
five-character prefix). Because the constraint lives in a commons, it is
type-checked and unit-tested without a KV binding, and the context flags mixes the
same type into its path parameters.
The context flags consumes bynk.cloudflare { Kv }, which locks the unit to
Cloudflare. Two actors sit over one service: User is a Bearer actor whose
identity is a non-empty UserId, and Editor is User where hasClaim("editor").
Authentication and authorisation stay distinct — a missing token is a 401, a
non-editor token a 403, both enforced at the boundary before any handler runs.
The service exposes four routes. GET /flags calls Kv.list(Some("flag:")) and
maps each key back through nameOf; GET /flags/:name reads one entry and decodes
it through Json.decode[Flag], returning NotFound for a missing key and
ServerError for a corrupt value rather than a silent undefined. The two writes,
PUT /flags/:name and DELETE /flags/:name, are bound by e: Editor and call
Kv.put (with Json.encode) and Kv.delete. The stored Flag record is just
{ enabled: Bool, description: String }.