Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .cursor/commands/gleam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Gleam on the BEAM

This is a Gleam application running on the BEAM (Erlang VM). Every solution must use the correct patterns for this platform. Solutions designed for other architectures (Node.js event loops, Go goroutines, Rust async, JVM threading, etc.) are not acceptable, even if they "work."

## The Standard

There is one correct way to solve most problems on the BEAM. Find it. Use it. Do not settle for a solution that merely functions — it must be idiomatically correct for Gleam/Erlang/OTP.

Before proposing any solution that involves concurrency, state management, fault tolerance, or inter-process communication, ask yourself: **"How would an experienced Erlang/OTP engineer solve this?"** Then find the Gleam equivalent.

## Platform Primitives (Use These)

### Processes and Actors

The BEAM's unit of concurrency is the lightweight process. State lives in processes, not in shared mutable memory.

- **Use `gleam_otp` actors** (`gleam/otp/actor`) for stateful services. Actors are the correct way to hold mutable state — not closures, not mutable references, not global variables.
- **Use `gleam_erlang` subjects and selectors** for message passing between processes.
- **Every long-lived stateful component should be an actor.** Caches, connection pools, rate limiters, session stores, background workers — these are all actors.
- **Never share state between processes via anything other than message passing.** No shared memory. No locks. No mutexes. This is not that kind of platform.

### Supervision Trees

Processes crash. That is expected and correct on the BEAM.

- **Use supervisors** (`gleam/otp/supervisor`) to manage process lifecycles. Every actor that matters should be supervised.
- **Design for failure.** "Let it crash" is not negligence — it is the architecture. Supervisors restart failed processes with known-good state.
- **Think about restart strategies.** One-for-one, one-for-all, rest-for-one — pick the right one for the failure domain.
- **Never try/catch your way out of a process crash.** Let the supervisor handle it. Recovery logic belongs in `init`, not in error handlers wrapped around every call.

### ETS (Erlang Term Storage)

ETS tables are the correct solution for shared read-heavy state that multiple processes need to access concurrently.

- **Use ETS for caches, lookup tables, and read-heavy shared state.** ETS provides concurrent reads without bottlenecking through a single actor's mailbox.
- **Do not use ETS as a general-purpose database.** It is in-memory and not persisted across restarts (unless you specifically set that up with DETS or manual persistence).
- **Owner process matters.** An ETS table is owned by the process that created it. If that process dies, the table is destroyed. Plan accordingly — typically the supervisor or a dedicated table-owner process should create tables.

### Process Links and Monitors

- **Use monitors** when you need to know if another process dies but don't want to die with it.
- **Use links** when processes should fail together.
- Understand the difference. Using the wrong one causes either silent failures or unnecessary cascading crashes.

## Erlang FFI

Gleam runs on the BEAM and can call Erlang directly. This is a strength, not a workaround.

- **Use Erlang FFI when Gleam lacks a wrapper** for BEAM functionality you need (e.g., `:ets`, `:timer`, `:crypto`, specific OTP behaviors).
- **Write thin FFI wrappers** — the Erlang code should be minimal, with the logic and types living in Gleam.
- **Always provide type-safe Gleam interfaces** over raw FFI calls. Never expose raw Erlang terms to calling code.
- **FFI is for platform access, not for bypassing Gleam's type system.** If you find yourself using FFI to work around Gleam's constraints, you are likely solving the wrong problem.

## Anti-Patterns (Never Do These)

- **Never use polling loops** where you should use process messaging or monitors.
- **Never use a single process as a bottleneck** for state that could live in ETS for concurrent reads.
- **Never spawn unsupervised processes** for anything that matters. If a process does real work, it belongs in a supervision tree.
- **Never store state in module-level variables or closures** pretending to be singletons. State belongs in actors.
- **Never implement your own process registry** when Erlang's built-in registry or `gproc`-style solutions exist.
- **Never use GenServer-style synchronous calls** when a cast (async message) would suffice and the caller doesn't need the result.
- **Never treat BEAM processes like OS threads.** They are cheap. Spawn thousands. Don't pool them like heavyweight threads.
- **Never reach for an external dependency** (Redis, a message queue, an in-memory cache library) when the BEAM already provides the primitive. ETS is your cache. Processes are your workers. Message passing is your queue.

## Decision Framework

When solving a problem, evaluate in this order:

1. **Can a pure function solve this?** No state, no processes needed. Just transform data. This is always preferred.
2. **Does this need state?** → Actor. Use `gleam/otp/actor`.
3. **Does this state need to survive crashes?** → Supervised actor with init-time recovery.
4. **Do multiple processes need to read this state concurrently?** → ETS table, owned by a supervised process.
5. **Does this need to react to process lifecycle events?** → Monitors or links.
6. **Does this need periodic work?** → Actor with `erlang.send_after` or `:timer` via FFI. Not a polling loop.
7. **Does this need to coordinate multiple actors?** → Supervisor tree with appropriate restart strategy.
8. **Does Gleam lack a wrapper for the BEAM feature I need?** → Erlang FFI with a type-safe Gleam wrapper.

## What "Correct" Means

A correct solution on this platform:

- Uses processes for isolation and concurrency, not threads or async/await
- Uses message passing for communication, not shared memory
- Uses supervisors for fault tolerance, not try/catch
- Uses ETS for shared read-heavy state, not a cache actor bottleneck
- Uses the existing BEAM ecosystem before reaching for external tools
- Compiles to idiomatic BEAM bytecode that an Erlang veteran would recognize as reasonable
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Updated mist streaming adapter to use actor-based `mist.chunked()` API
instead of the removed `mist.Streaming` response variant. Dream's public
streaming API (`stream_response`, `sse_response`, `ResponseBody.Stream`)
is unchanged.

## [2.4.1] - 2026-03-11

### Fixed
Expand Down
3 changes: 1 addition & 2 deletions examples/custom_context/gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ licences = ["MIT"]
dream = { path = "../.." }
dream_http_client = { path = "../../modules/http_client" }
gleam_http = ">= 4.3.0 and < 5.0.0"
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
mist = ">= 5.0.3 and < 6.0.0"
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
20 changes: 10 additions & 10 deletions examples/custom_context/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
# You typically do not need to edit this file

packages = [
{ name = "dream", version = "0.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_json", "gleam_otp", "gleam_stdlib", "gleam_time", "gleam_yielder", "marceau", "mist", "simplifile"], source = "local", path = "../.." },
{ name = "dream_http_client", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib", "gleam_yielder"], source = "local", path = "../../modules/http_client" },
{ name = "cowlib", version = "2.16.0", build_tools = ["make", "rebar3"], requirements = [], otp_app = "cowlib", source = "hex", outer_checksum = "7F478D80D66B747344F0EA7708C187645CFCC08B11AA424632F78E25BF05DB51" },
{ name = "dream", version = "2.4.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_json", "gleam_otp", "gleam_stdlib", "gleam_time", "gleam_yielder", "marceau", "mist", "simplifile"], source = "local", path = "../.." },
{ name = "dream_http_client", version = "5.2.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_otp", "gleam_stdlib", "gleam_yielder", "gun", "simplifile"], source = "local", path = "../../modules/http_client" },
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
{ name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
{ name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
{ name = "gleam_time", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "D560E672C7279C89908981E068DF07FD16D0C859DCA266F908B18F04DF0EB8E6" },
{ name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" },
{ name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" },
{ name = "glisten", version = "9.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging"], otp_app = "glisten", source = "hex", outer_checksum = "D92808C66F7D3F22F2289CD04CBA8151757AAE9CB3D86992F0C6DE32A41205E1" },
{ name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" },
{ name = "gun", version = "2.2.0", build_tools = ["make", "rebar3"], requirements = ["cowlib"], otp_app = "gun", source = "hex", outer_checksum = "76022700C64287FEB4DF93A1795CFF6741B83FB37415C40C34C38D2A4645261A" },
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
{ name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
{ name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" },
{ name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" },
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
{ name = "mist", version = "6.0.2", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], source = "git", repo = "https://github.com/TrustBound/mist.git", commit = "ae358876d2d6763e8febb977221797fc42bffcc0" },
{ name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" },
]

[requirements]
dream = { path = "../.." }
dream_http_client = { path = "../../modules/http_client" }
gleam_http = { version = ">= 4.3.0 and < 5.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
mist = { version = ">= 5.0.3 and < 6.0.0" }
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ fn make_request_and_respond(user_id: String, post_id: String) -> Response {
text_response(status.ok, post_view.format_show(user_id, post_id, body))
Error(client.ResponseError(response: client.HttpResponse(body: body, ..))) ->
text_response(status.internal_server_error, post_view.format_error(body))
Error(client.RequestError(message: error)) ->
text_response(status.internal_server_error, post_view.format_error(error))
Error(client.RequestError(error: transport_error)) ->
text_response(
status.internal_server_error,
post_view.format_error(client.transport_error_to_string(transport_error)),
)
}
}
1 change: 0 additions & 1 deletion examples/database/gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ licences = ["MIT"]
dream = { path = "../.." }
dream_json = { path = "../../modules/json" }
dream_postgres = { path = "../../modules/postgres" }
mist = ">= 5.0.3 and < 6.0.0"
pog = ">= 4.0.0 and < 5.0.0"
cigogne = ">= 5.0.0 and < 6.0.0"
gleam_json = ">= 2.2.0 and < 4.0.0"
Expand Down
Loading
Loading