gflare
Zero-glue Gleam framework for Cloudflare Workers. Write Gleam, deploy to Cloudflare — no index.js, no wrangler.toml editing, no JavaScript.
Quick Start
# Add to your project
gleam add gflare
# Initialize Cloudflare Workers in your project
gleam run -m gflare -- init
# Or add to an existing project, then:
gleam run -m gflare -- dev
Minimal Example
import gflare/bindings.{type Env}
import gflare/worker.{type Context}
import gflare/request.{type HttpRequest}
import gflare/response
pub fn fetch(request: HttpRequest, env: Env, ctx: Context) {
response.new(200)
|> response.set_body("Hello from Gleam!")
|> promise.resolve
}
CLI Commands
| Command | Description |
|---|---|
gleam run -m gflare -- init | Initialize Cloudflare Workers in current project |
gleam run -m gflare -- build | Build for Cloudflare Workers |
gleam run -m gflare -- dev | Build and start local dev server |
gleam run -m gflare -- deploy | Build and deploy to Cloudflare |
gleam run -m gflare -- --help | Show help |
Configuration
Add a [cloudflare] section to your gleam.toml:
[cloudflare]
name = "my-worker"
compatibility_date = "2025-01-01"
[cloudflare.bindings]
kv = ["CACHE", "SESSIONS"]
d1 = ["DB"]
r2 = ["ASSETS"]
queues_producers = ["EVENTS"]
queues_consumers = ["events"]
[cloudflare.durable_objects]
classes = [
{ name = "Counter", module = "my_worker/durable_objects/counter" }
]
[cloudflare.vars]
ENVIRONMENT = "production"
Note: Turso doesn’t need a binding — just pass URL and token directly via
turso.connect()or read from env vars withbindings.var()/bindings.secret().
Bindings
Bindings Resolution
All bindings are resolved from the Cloudflare Worker env object:
import gflare/bindings
pub fn fetch(request, env: Env, ctx: Context) {
let assert Ok(cache) = bindings.kv(env, "CACHE")
let assert Ok(db) = bindings.d1(env, "DB")
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
let assert Ok(counter_ns) = bindings.durable_object(env, "COUNTER")
let assert Ok(queue) = bindings.queue_producer(env, "EVENTS")
let assert Ok(api_key) = bindings.secret(env, "API_KEY")
let assert Ok(env) = bindings.var(env, "ENVIRONMENT")
// ... use bindings
}
KV (Key-Value Storage)
import gflare/kv
pub fn fetch(request, env: Env, ctx: Context) {
let assert Ok(cache) = bindings.kv(env, "CACHE")
// Get a value
use result <- promise.await(kv.get(cache, "greeting"))
case result {
Ok(value) -> response.new(200) |> response.set_body(value) |> promise.resolve
Error(_) -> response.new(404) |> response.set_body("Not found") |> promise.resolve
}
}
pub fn put_example(cache) {
// Put with default options
use _ <- promise.await(kv.put(cache, "key", "value", kv.put_options()))
// Put with expiration (TTL)
let opts = kv.put_options_with(expiration: None, expiration_ttl: Some(3600))
use _ <- promise.await(kv.put(cache, "session:123", data, opts))
// Get with cache TTL
let opts = kv.get_options_with(type_: "json", cache_ttl: Some(60))
use result <- promise.await(kv.get(cache, "key", opts))
// List keys
let opts = kv.list_options()
use result <- promise.await(kv.list(cache, opts))
case result {
Ok(list_result) -> {
// list_result.keys is a List(KvKey)
// list_result.list_complete is Bool
// list_result.cursor is Option(String) for pagination
}
Error(e) -> io.println_error(error.to_string(e))
}
// Delete a key
use _ <- promise.await(kv.delete(cache, "old_key"))
}
D1 (SQLite Database)
import gflare/d1
import gleam/dynamic/decode
// Define a decoder for your query results
fn user_decoder() {
use id <- decode.field("id", decode.int)
use name <- decode.field("name", decode.string)
use email <- decode.field("email", decode.string)
decode.success(User(id:, name:, email:))
}
pub fn fetch_users(request, env: Env, ctx: Context) {
let assert Ok(db) = bindings.d1(env, "DB")
// Simple query
let stmt = d1.prepare(db, "SELECT * FROM users LIMIT 10")
use result <- promise.await(d1.all(stmt))
case result {
Ok(result) -> {
// result.results is List(Dynamic) — decode each row
let users = list.map(result.results, fn(row) {
let assert Ok(user) = decode.run(row, user_decoder())
user
})
response.new(200) |> response.json(json.array(users, user_to_json)) |> promise.resolve
}
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn insert_user(request, env: Env, ctx: Context) {
let assert Ok(db) = bindings.d1(env, "DB")
// Prepared statement with parameters
let stmt = d1.prepare(db, "INSERT INTO users (name, email) VALUES (?, ?)")
let stmt = d1.bind(stmt, [dynamic.from("Alice"), dynamic.from("alice@example.com")])
use result <- promise.await(d1.run(stmt))
case result {
Ok(result) -> {
// result.meta.last_row_id contains the inserted row ID
response.new(201) |> response.set_body("Created") |> promise.resolve
}
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn get_user_by_id(request, env: Env, ctx: Context) {
let assert Ok(db) = bindings.d1(env, "DB")
// Get first row
let stmt = d1.prepare(db, "SELECT * FROM users WHERE id = ?")
let stmt = d1.bind(stmt, [dynamic.from(42)])
use result <- promise.await(d1.first(stmt))
case result {
Ok(Some(row)) -> {
let assert Ok(user) = decode.run(row, user_decoder())
response.new(200) |> response.json(user_to_json(user)) |> promise.resolve
}
Ok(None) -> response.new(404) |> response.set_body("User not found") |> promise.resolve
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn run_raw_sql(request, env: Env, ctx: Context) {
let assert Ok(db) = bindings.d1(env, "DB")
// Execute raw SQL
use result <- promise.await(d1.exec(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"))
response.new(200) |> response.set_body("OK") |> promise.resolve
}
### Turso (Database over HTTP)
Turso uses only `fetch` — no npm packages needed. Perfect for Cloudflare Workers.
```gleam
import gflare/turso
pub fn fetch(request, env: Env, ctx: Context) {
// Read config from environment variables
let assert Ok(url) = bindings.var(env, "TURSO_DATABASE_URL")
let assert Ok(token) = bindings.secret(env, "TURSO_AUTH_TOKEN")
let config = turso.connect(url, token)
// Simple query
use result <- promise.await(turso.execute(config, "SELECT * FROM users", []))
case result {
Ok(result) -> {
// result.rows contains the rows
// result.columns contains column names
// result.rows_affected contains affected row count
response.new(200) |> response.set_body("Found users") |> promise.resolve
}
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn query_with_params(request, env: Env, ctx: Context) {
let assert Ok(url) = bindings.var(env, "TURSO_DATABASE_URL")
let assert Ok(token) = bindings.secret(env, "TURSO_AUTH_TOKEN")
let config = turso.connect(url, token)
// Query with parameters
use result <- promise.await(turso.execute(
config,
"SELECT * FROM users WHERE id = ?",
[turso.int(42)],
))
// Handle result...
}
pub fn batch_example(request, env: Env, ctx: Context) {
let assert Ok(url) = bindings.var(env, "TURSO_DATABASE_URL")
let assert Ok(token) = bindings.secret(env, "TURSO_AUTH_TOKEN")
let config = turso.connect(url, token)
// Batch execution
use result <- promise.await(turso.batch(
config,
[
#("INSERT INTO users (name) VALUES (?)", [turso.text("Alice")]),
#("INSERT INTO users (name) VALUES (?)", [turso.text("Bob")]),
],
turso.Write,
))
// Handle result...
}
pub fn transaction_example(request, env: Env, ctx: Context) {
let assert Ok(url) = bindings.var(env, "TURSO_DATABASE_URL")
let assert Ok(token) = bindings.secret(env, "TURSO_AUTH_TOKEN")
let config = turso.connect(url, token)
// Transaction (rolls back on any error)
use result <- promise.await(turso.transaction(config, [
#("UPDATE accounts SET balance = balance - 100 WHERE id = 1", []),
#("UPDATE accounts SET balance = balance + 100 WHERE id = 2", []),
]))
// Handle result...
}
R2 (Object Storage)
import gflare/r2
pub fn get_file(request, env: Env, ctx: Context) {
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
let key = request.path(request)
use result <- promise.await(r2.get(bucket, key))
case result {
Ok(body) -> {
// Read as text
use text <- promise.await(r2.read_text(body))
case text {
Ok(content) -> response.new(200) |> response.set_body(content) |> promise.resolve
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
Error(_) -> response.new(404) |> response.set_body("File not found") |> promise.resolve
}
}
pub fn upload_file(request, env: Env, ctx: Context) {
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
use body <- promise.await(request.array_buffer(request))
case body {
Ok(bytes) -> {
let opts = r2.put_options_with(
http_metadata: Some(r2.HttpMetadata(
content_type: Some("text/plain"),
content_disposition: None,
content_encoding: None,
cache_control: None,
cache_expiry: None,
)),
custom_metadata: None,
)
use result <- promise.await(r2.put(bucket, "uploads/file.txt", bytes, opts))
case result {
Ok(obj) -> response.new(200) |> response.set_body("Uploaded: " <> obj.key) |> promise.resolve
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
Error(e) -> response.new(400) |> response.set_body(e) |> promise.resolve
}
}
pub fn list_files(request, env: Env, ctx: Context) {
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
let opts = r2.list_options_with(
prefix: Some("uploads/"),
cursor: None,
delimiter: None,
limit: Some(100),
include: None,
)
use result <- promise.await(r2.list(bucket, opts))
case result {
Ok(list_result) -> {
// list_result.objects is List(ListObject)
// list_result.truncated is Bool
// list_result.delimited_prefixes is List(String)
response.new(200) |> response.set_body("Found files") |> promise.resolve
}
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn delete_file(request, env: Env, ctx: Context) {
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
use _ <- promise.await(r2.delete(bucket, "old-file.txt"))
response.new(204) |> promise.resolve
}
pub fn file_metadata(request, env: Env, ctx: Context) {
let assert Ok(bucket) = bindings.r2(env, "ASSETS")
use result <- promise.await(r2.head(bucket, "file.txt"))
case result {
Ok(meta) -> response.new(200) |> response.set_body(meta.etag) |> promise.resolve
Error(e) -> response.new(404) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
Queues (Message Queue)
import gflare/queue
import gleam/json
// Producer: send messages
pub fn enqueue_job(request, env: Env, ctx: Context) {
let assert Ok(q) = bindings.queue_producer(env, "EVENTS")
let message = json.object([
#("type", json.string("email")),
#("to", json.string("user@example.com")),
#("subject", json.string("Welcome!")),
])
use _ <- promise.await(queue.send(q, message))
response.new(200) |> response.set_body("Job queued") |> promise.resolve
}
// Producer: send batch
pub fn enqueue_batch(request, env: Env, ctx: Context) {
let assert Ok(q) = bindings.queue_producer(env, "EVENTS")
let messages = [
json.object([#("type", json.string("email")), #("to", json.string("a@test.com"))]),
json.object([#("type", json.string("email")), #("to", json.string("b@test.com"))]),
]
use _ <- promise.await(queue.send_batch(q, messages))
response.new(200) |> response.set_body("Batch queued") |> promise.resolve
}
// Consumer: process messages
pub fn queue(batch, env: Env, ctx: Context) {
list.each(batch.messages, fn(msg) {
let body = queue.message_body(msg)
let id = queue.message_id(msg)
let attempts = queue.message_attempts(msg)
// Process the message...
io.println("Processing message " <> id <> " (attempt " <> int.to_string(attempts) <> ")")
// Acknowledge on success
let assert Ok(_) = queue.ack(msg)
// Or retry on failure
// let assert Ok(_) = queue.retry(msg)
})
promise.resolve(Nil)
}
Durable Objects
import gflare/durable_object
pub fn fetch(request, env: Env, ctx: Context) {
let assert Ok(ns) = bindings.durable_object(env, "COUNTER")
// Get a deterministic ID from a name
let id = durable_object.id_from_name(ns, "user:42")
// Get a stub (proxy to the DO instance)
let stub = durable_object.get_stub(ns, id)
// Call the DO
use result <- promise.await(durable_object.get(stub))
case result {
Ok(data) -> response.new(200) |> response.json(data) |> promise.resolve
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
pub fn increment_counter(request, env: Env, ctx: Context) {
let assert Ok(ns) = bindings.durable_object(env, "COUNTER")
let id = durable_object.id_from_name(ns, "global")
let stub = durable_object.get_stub(ns, id)
use _ <- promise.await(durable_object.set(stub, "count", json.int(1)))
response.new(200) |> response.set_body("Incremented") |> promise.resolve
}
pub fn schedule_alarm(request, env: Env, ctx: Context) {
let assert Ok(ns) = bindings.durable_object(env, "COUNTER")
let id = durable_object.id_from_name(ns, "scheduler")
let stub = durable_object.get_stub(ns, id)
// Set alarm 60 seconds from now
let timestamp = 1_700_000_000_000 + 60_000
use _ <- promise.await(durable_object.set_alarm(stub, timestamp))
response.new(200) |> response.set_body("Alarm scheduled") |> promise.resolve
}
Worker Context
import gflare/worker
pub fn fetch(request, env: Env, ctx: Context) {
// Extend worker lifetime for background work
use _ <- promise.await(background_task(env))
worker.wait_until(ctx, background_promise)
// Or pass through to origin on exceptions
worker.pass_through_on_exception(ctx)
response.new(200) |> response.set_body("OK") |> promise.resolve
}
Request Helpers
import gflare/request
pub fn handler(request, env: Env, ctx: Context) {
let url = request.url(request)
let method = request.method(request)
let headers = request.headers(request)
// headers is List(#(String, String))
// Read body as text
use body <- promise.await(request.text(request))
case body {
Ok(text) -> response.new(200) |> response.set_body(text) |> promise.resolve
Error(e) -> response.new(400) |> response.set_body(e) |> promise.resolve
}
// Or read as JSON
use json_body <- promise.await(request.json(request))
case json_body {
Ok(data) -> {
// data is Dynamic — decode it
let assert Ok(name) = decode.run(data, decode.field("name", decode.string, decode.success))
response.new(200) |> response.set_body("Hello " <> name) |> promise.resolve
}
Error(e) -> response.new(400) |> response.set_body(e) |> promise.resolve
}
// Or read raw bytes
use bytes <- promise.await(request.array_buffer(request))
// bytes is BitArray
}
Response Helpers
import gflare/response
import gleam/json
// Text response
response.new(200)
|> response.set_body("Hello, World!")
|> response.set_header("X-Custom", "value")
// JSON response
response.new(200)
|> response.json(json.object([#("status", json.string("ok"))]))
// Binary response
response.new(200)
|> response.bytes(<<30, 56, 10>>)
// Empty response
response.new(204)
// Redirect
response.redirect("https://example.com", 302)
// Pipe-friendly: all functions return Response
response.new(200)
|> response.set_header("Cache-Control", "no-cache")
|> response.set_body("No cache here!")
JSON Utilities
import gflare/json_util
import gleam/json
// Create JSON objects (filters out null values)
let data = json_util.sparse([
#("name", json.string("Alice")),
#("age", json.null()), // This will be omitted
#("active", json.bool(True)),
])
// Option to JSON
let value = json_util.option_to_json(Some(42), json.int)
// value == json.int(42)
let nothing = json_util.option_to_json(None, json.int)
// nothing == json.null()
// Parse JSON string
case json_util.parse("{\"key\": \"value\"}") {
Ok(dynamic) -> // use dynamic
Error(msg) -> io.println_error(msg)
}
// Decoder helpers
let decoder = {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
decode.success(Person(name:, age:))
}
Error Handling
All binding operations return Result(T, gflare/error.Error):
import gflare/error
case result {
Ok(value) -> // use value
Error(err) -> io.println_error(error.to_string(err))
// Error variants:
// KvError(message)
// D1Error(message)
// R2Error(message)
// DurableObjectError(message)
// QueueError(message)
// BindingNotFound(name)
// EncodingError(message)
// DecodingError(message)
}
Full Example
A complete worker with KV caching, D1 database, and Turso:
import gflare/bindings.{type Env}
import gflare/worker.{type Context}
import gflare/request.{type HttpRequest}
import gflare/response
import gflare/kv
import gflare/d1
import gflare/turso
import gflare/json_util
import gleam/dynamic/decode
import gleam/json
import gleam/list
import gleam/option.{None, Some}
pub type User {
User(id: Int, name: String, email: String)
}
fn user_decoder() {
use id <- decode.field("id", decode.int)
use name <- decode.field("name", decode.string)
use email <- decode.field("email", decode.string)
decode.success(User(id:, name:, email:))
}
fn user_to_json(user: User) -> json.Json {
json.object([
#("id", json.int(user.id)),
#("name", json.string(user.name)),
#("email", json.string(user.email)),
])
}
pub fn fetch(request: HttpRequest, env: Env, ctx: Context) {
let assert Ok(cache) = bindings.kv(env, "CACHE")
// Try KV cache first
use cached <- promise.await(kv.get(cache, "users"))
case cached {
Ok(json_str) -> {
// Cache hit — return cached data
let assert Ok(data) = json_util.parse(json_str)
response.new(200) |> response.json(data) |> promise.resolve
}
Error(_) -> {
// Cache miss — query Turso (better for transactions than D1)
let assert Ok(url) = bindings.var(env, "TURSO_DATABASE_URL")
let assert Ok(token) = bindings.secret(env, "TURSO_AUTH_TOKEN")
let config = turso.connect(url, token)
use result <- promise.await(turso.execute(config, "SELECT * FROM users", []))
case result {
Ok(result) -> {
let users =
list.map(result.rows, fn(row) {
let assert Ok(user) = decode.run(user_decoder(), decode.dynamic)
user
})
let json_data = json.array(users, user_to_json)
// Cache for 5 minutes
let cache_opts = kv.put_options_with(expiration: None, expiration_ttl: Some(300))
use _ <- promise.await(kv.put(cache, "users", json_util.sparse([#("data", json_data)]), cache_opts))
response.new(200) |> response.json(json_data) |> promise.resolve
}
Error(e) -> response.new(500) |> response.set_body(error.to_string(e)) |> promise.resolve
}
}
}
}
How It Works
┌─────────────────────────────────────────────────┐
│ Your Gleam code (handlers + binding calls) │
├─────────────────────────────────────────────────┤
│ gflare library (types, FFI, wrappers) │
├─────────────────────────────────────────────────┤
│ gleam build │
│ → outputs .mjs files in build/dev/javascript/ │
├─────────────────────────────────────────────────┤
│ gflare CLI (detects handlers, generates glue) │
│ → generates index.js + wrangler.toml │
├─────────────────────────────────────────────────┤
│ esbuild (bundles into single file) │
├─────────────────────────────────────────────────┤
│ wrangler dev / wrangler deploy │
└─────────────────────────────────────────────────┘
gleam buildcompiles your Gleam to.mjsfiles- The CLI scans the compiled output for exported handlers (
fetch,queue, etc.) - It generates
index.js(Cloudflare Worker entrypoint) andwrangler.toml esbuildbundles everything into a single filewranglerruns locally or deploys to Cloudflare
License
MIT — see LICENSE for details.