gflare

Zero-glue Gleam framework for Cloudflare Workers. Write Gleam, deploy to Cloudflare — no index.js, no wrangler.toml editing, no JavaScript.

Package Version Hex Docs

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

CommandDescription
gleam run -m gflare -- initInitialize Cloudflare Workers in current project
gleam run -m gflare -- buildBuild for Cloudflare Workers
gleam run -m gflare -- devBuild and start local dev server
gleam run -m gflare -- deployBuild and deploy to Cloudflare
gleam run -m gflare -- --helpShow 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 with bindings.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                 │
└─────────────────────────────────────────────────┘
  1. gleam build compiles your Gleam to .mjs files
  2. The CLI scans the compiled output for exported handlers (fetch, queue, etc.)
  3. It generates index.js (Cloudflare Worker entrypoint) and wrangler.toml
  4. esbuild bundles everything into a single file
  5. wrangler runs locally or deploys to Cloudflare

License

MIT — see LICENSE for details.

Search Document