Glixir 🌟
A Safe(ish) OTP interop between Gleam and Elixir/Erlang
Bridge the gap between Gleam's type safety and the battle-tested OTP ecosystem. Use GenServers, Supervisors, Agents, Registry, and more from Gleam with confidence.
Features
- ✅ GenServer - Type-safe calls to Elixir GenServers
- ✅ DynamicSupervisor - Runtime process supervision and management
- ✅ Agent (now generic!) - Type-safe state management with async operations
- ✅ Registry - Dynamic process registration and Subject lookup
- 🚧 Task - Async task execution (coming soon)
- ✅ Phoenix.PubSub - Distributed messaging with JSON-based type safety
- ✅ libcluster - Automatic node discovery and clustering for distributed systems
- ✅ Zero overhead - Direct BEAM interop with clean Elixir helpers
- ✅ Gradual adoption - Use alongside existing Elixir code
- ✅ syn - Distributed process registry and PubSub coordination
Type Safety & Phantom Types
A note from your neighborhood type enthusiast:
Some
glixirAPIs (notably process calls, GenServer, Registry) still require passingDynamicvalues—this is the price of seamless BEAM interop and runtime dynamism. While decoders on return values help catch mismatches, full compile-time type safety isn't always possible... yet.But here's the good news: We're actively rolling out phantom types and generics across the API, banishing
Dynamicwherever possible and making misused actors a compile-time relic.
Type safety level
GenServer: [■■■■■■■■□□] 80% - Request/reply types enforced by Gleam, decoder required, but runtime BEAM interop can still fail if types disagree.
Supervisor: [■■■■■■■■■□] 90% - Phantom-typed with compile-time child spec validation, args/replies bounded by generics.
Registry: [■■■■■■■■■□] 90% - Phantom-typed with compile-time key/message validation, requires key encoders.
Agent: [■■■■■■■■■■] 100% - State and API fully generic and type safe!
PubSub: [■■■■■■■■□□] 80% - JSON-based type safety with user-defined encoders/decoders. Phantom-typed for message_type.
libcluster: [■■■■■■■□□□] 70% - Node discovery works, cluster membership is runtime dynamic
syn: [■■■■■■■■□□] 80% - Distributed coordination with type-safe message patterns, runtime node discovery.
Task: [□□□□□□□□□□] 0% - Not started.
Full green bars are the dream; until then, decoders are your seatbelt!
Why Not 100% Type-Safe Now?
Blame Erlang! (Just kidding, blame dynamic process boundaries.) You get strict safety inside Gleam, but as soon as you jump the BEAM-to-BEAM fence, the runtime is your playground. So, until Gleam's type system can tame Elixir's wild world, we work with decoders and are pushing hard to get you closer to total type bliss.
⚠️ Caveats: BEAM Interop Trade-offs
Glixir provides bounded type safety - the maximum safety possible while maintaining full OTP functionality. Some limitations are inherent to BEAM interop, not design flaws:
The Safety Spectrum
- Pure Gleam: 100% compile-time safe, but no distributed features
- Glixir: 70-90% compile-time safe + runtime validation, full OTP power
- Raw FFI: ~20% safe, full OTP power, high risk
Unavoidable BEAM Realities
- Process discovery - distributed systems have runtime process existence
- Module loading - OTP's dynamic nature requires string module names
- Cross-language boundaries - serialization between Gleam and Elixir
What You Get
✅ Compile-time: Prevents category errors, type mixing, wrong message types
Bottom Line: Glixir is the sweet spot - maximum practical safety with essential functionality that core Gleam simply doesn't provide.
Want to help speed this up? File issues, suggest API improvements, or just cheer us on in the repo!
⚡ Atom Safety & Fail-Fast API
glixir now requires all process, registry, and module names to be existing atoms—typos or missing modules crash immediately with {bad_atom, ...}.
- Avoids BEAM atom leaks and silent bugs.
- If you typo or use a non-existent module, you'll see a loud error.
- Pro-tip: Always reference the real module, or pre-load it in Elixir before calling from Gleam.
This makes glixir safer by default—don't trust user input as atom names!
libcluster - Automatic Node Discovery & Clustering
Scale your Gleam apps across multiple nodes with zero configuration! 🌐 libcluster provides automatic node discovery and clustering for distributed BEAM applications, with built-in support for Fly.io, Kubernetes, AWS, and local development.
import glixir/libcluster import gleam/erlang/os import logging pub fn main() { // Auto-detect environment and start clustering let _ = case os.get_env("FLY_APP_NAME") { Ok(app_name) -> { // Running on Fly.io - automatic DNS-based discovery libcluster.start_clustering_fly(app_name) } Error(_) -> { // Local development - use EPMD for simplicity libcluster.start_clustering_local("my_app") } } // Check cluster status io.println("Node: " <> libcluster.current_node_name()) io.println("Connected to: " <> string.inspect(libcluster.connected_node_names())) // Your distributed app starts here // syn will automatically sync across all discovered nodes! start_distributed_app() } // Production deployment with custom DNS pub fn production_clustering() { // DNS-based discovery for production let assert Ok(_) = libcluster.start_clustering_dns( app_name: "my_app", query: "my_app.service.consul", // Consul, Kubernetes DNS, etc polling_interval: 5000 ) // Monitor cluster membership case libcluster.is_clustered() { True -> { let nodes = libcluster.connected_nodes() logging.info("Clustered with " <> int.to_string(list.length(nodes)) <> " nodes") } False -> { logging.warning("Running in single-node mode") } } } // Combine with syn for distributed process coordination pub fn distributed_workers() { // Start clustering let assert Ok(_) = libcluster.start_clustering_fly("worker_pool") // syn automatically discovers all nodes via libcluster! syn.init_scopes(["workers"]) // This registration is visible across ALL nodes syn.register("workers", "worker_1", self()) // Find workers on ANY node in the cluster case syn.whereis("workers", "worker_1") { Ok(#(pid, node)) -> { io.println("Found worker on node: " <> atom.to_string(node)) } Error(_) -> io.println("Worker not found in cluster") } }
Supported Clustering Strategies:
- Fly.io - Automatic DNS discovery via APP_NAME.internal
- Kubernetes - Service discovery via DNS or API
- AWS EC2 - Tag-based instance discovery
- Local Dev - EPMD for testing multi-node locally
- Custom DNS - Any DNS-based service discovery Use Cases:
- Distributed job queues across multiple machines
- Fault-tolerant services with automatic failover
- Load balancing across geographic regions
- Consensus algorithms and leader election
- Real-time data synchronization
syn - Distributed Process Coordination
Distributed service discovery and event streaming across BEAM nodes! 🌐
Use Erlang's battle-tested syn library for distributed coordination, consensus algorithms, and fault-tolerant process management.
import glixir/syn import gleam/json pub fn distributed_example() { // Initialize scopes at application startup syn.init_scopes(["worker_pools", "coordination"]) // Register this process in a distributed pool let load_info = #(cpu_usage: 0.3, queue_size: 5) let assert Ok(_) = syn.register_worker("image_processors", "worker_1", load_info) // Find workers across the cluster case syn.find_worker("image_processors", "worker_1") { Ok(#(pid, #(cpu_usage, queue_size))) -> { // Send work to the least loaded worker process.send(pid, ProcessImage("photo.jpg")) } Error(_) -> // Worker not available, try another } // Join coordination group for consensus let assert Ok(_) = syn.join_coordination("leader_election") // Broadcast status for distributed coordination let status = json.object([ #("node", json.string("worker_node_1")), #("load", json.float(0.3)), #("available", json.bool(True)) ]) let assert Ok(nodes_notified) = syn.broadcast_status("health_check", status) io.println("Status sent to " <> int.to_string(nodes_notified) <> " nodes") } // Advanced: Custom coordination patterns pub fn consensus_example() { // Register with metadata for consensus algorithms let machine_status = #(queue_lengths: #(0, 5, 2), capacity: 100) let assert Ok(_) = syn.register("machines", "machine_1", machine_status) // Publish for distributed decision making let queue_status = json.object([ #("machine_id", json.string("machine_1")), #("free_queue", json.int(0)), #("paid_queue", json.int(5)), #("capacity", json.int(100)) ]) let assert Ok(_) = syn.publish_json("coordination", "load_balancing", queue_status, fn(j) { j }) } ## Installation ```sh gleam add glixir
Add the Elixir helper modules to your project:
# lib/glixir_supervisor.ex and lib/glixir_registry.ex # Copy the helper modules from the docs
Quick Start
PubSub - Type-Safe Distributed Messaging
Real-time event broadcasting with JSON-based type safety! 📡
import glixir import gleam/json import gleam/dynamic/decode import gleam/erlang/atom // Define your message types pub type UserEvent { PageView(user_id: String, page: String) Purchase(user_id: String, amount: Float) } // Create encoder/decoder for type safety pub fn encode_user_event(event: UserEvent) -> String { case event { PageView(user_id, page) -> json.object([ #("type", json.string("page_view")), #("user_id", json.string(user_id)), #("page", json.string(page)) ]) |> json.to_string Purchase(user_id, amount) -> json.object([ #("type", json.string("purchase")), #("user_id", json.string(user_id)), #("amount", json.float(amount)) ]) |> json.to_string } } pub fn decode_user_event(json_string: String) -> Result(UserEvent, String) { // Your custom decoder logic here json.decode(json_string, dynamic.decode3( PageView, dynamic.field("user_id", decode.string), dynamic.field("page", decode.string), dynamic.field("type", decode.string) )) } // Handler function for PubSub messages pub fn handle_user_events(json_message: String) -> Nil { case decode_user_event(json_message) { Ok(PageView(user_id, page)) -> { io.println("User " <> user_id <> " viewed " <> page) } Ok(Purchase(user_id, amount)) -> { io.println("User " <> user_id <> " purchased $" <> float.to_string(amount)) } Error(_) -> { io.println("Failed to decode user event") } } } pub fn pubsub_example() { // Start a phantom-typed PubSub system let assert Ok(_pubsub: glixir.PubSub(UserEvent)) = glixir.pubsub_start(atom.create("user_events")) // Subscribe with your handler function let assert Ok(_) = glixir.pubsub_subscribe( atom.create("user_events"), "user:metrics", "my_app", // Your module name "handle_user_events" // Your handler function ) // Broadcast type-safe events from anywhere let page_event = PageView("user_123", "/dashboard") let assert Ok(_) = glixir.pubsub_broadcast( atom.create("user_events"), "user:metrics", page_event, encode_user_event // Your encoder ) let purchase_event = Purchase("user_123", 99.99) let assert Ok(_) = glixir.pubsub_broadcast( atom.create("user_events"), "user:metrics", purchase_event, encode_user_event ) // Cleanup when done let assert Ok(_) = glixir.pubsub_unsubscribe( atom.create("user_events"), "user:metrics" ) }
Direct Actor Targeting Pattern:
// Perfect for metric actors that need their own identity pub fn handle_metric_update(actor_id: String, json_message: String) -> Nil { case decode_metric_message(json_message) { Ok(metric) -> { // Update this specific actor's metrics io.println("Actor " <> actor_id <> " received metric: " <> metric.name) // ... update actor state } Error(_) -> { io.println("Invalid metric for actor: " <> actor_id) } } }
// Subscribe each metric actor with its own ID let assert Ok() = glixir.pubsub_subscribe_with_registry_key( atom.create("metrics_pubsub"), "metric:updates", "my_app", "handle_metric_update", "metric_actor" <> actor_id // Each actor gets its own key )
Multi-Message Handler Example:
// Single handler can process multiple message types! pub fn handle_all_events(json_message: String) -> Nil { case decode_user_event(json_message) { Ok(user_event) -> handle_user_event(user_event) Error(_) -> case decode_system_event(json_message) { Ok(system_event) -> handle_system_event(system_event) Error(_) -> io.println("Unknown message type: " <> json_message) } } }
Agent State Management (Now Type Safe!)
import glixir import gleam/dynamic/decode import gleam/erlang/atom pub fn main() { // Start an agent with initial state (Agent(Int)) let assert Ok(counter) = glixir.start_agent(fn() { 42 }) // Get state with a decoder (type safe!) let assert Ok(value) = glixir.get_agent(counter, fn(x) { x }, decode.int) io.debug(value) // 42 // Update state let assert Ok(_) = glixir.update_agent(counter, fn(n) { n + 10 }) // Get and update in one operation let assert Ok(old_value) = glixir.get_and_update_agent( counter, fn(n) { #(n, n * 2) }, decode.int ) io.debug(old_value) // 52 // Stop the agent with a reason (now explicit) let assert Ok(_) = glixir.stop_agent(counter, atom.create("normal")) }
Registry - Dynamic Actor Discovery
import glixir import gleam/erlang/process import gleam/erlang/atom import gleam/dynamic import gleam/io pub type UserMessage { RecordMetric(name: String, value: Float) GetStats } pub fn actor_discovery_example() { // Start phantom-typed registry for actor lookup let assert Ok(_registry: glixir.Registry(atom.Atom, UserMessage)) = glixir.start_registry(atom.create("user_actors")) // Start a type-safe dynamic supervisor let assert Ok(supervisor) = glixir.start_dynamic_supervisor_named( atom.create("user_supervisor") ) // String encoder for user ID args fn user_id_encode(user_id: String) -> List(dynamic.Dynamic) { [dynamic.string(user_id)] } // Simple decoder for replies fn simple_decode(_d: dynamic.Dynamic) -> Result(String, String) { Ok("started") } // Create a type-safe child spec let user_spec = glixir.child_spec( id: "user_123", module: "MyApp.UserActor", function: "start_link", args: "user_123", // Typed as String! restart: glixir.permanent, shutdown_timeout: 5000, child_type: glixir.worker, encode: user_id_encode, ) // Start the child with compile-time type safety case glixir.start_dynamic_child(supervisor, user_spec, user_id_encode, simple_decode) { glixir.ChildStarted(_user_pid, _reply) -> { // Actor registers itself in the registry with typed key let user_subject = process.new_subject() let assert Ok(_) = glixir.register_subject( atom.create("user_actors"), atom.create("user_123"), // Typed Atom key user_subject, glixir.atom_key_encoder // Required encoder ) // Later... find and message the actor from anywhere! case glixir.lookup_subject( atom.create("user_actors"), atom.create("user_123"), glixir.atom_key_encoder ) { Ok(subject) -> { process.send(subject, RecordMetric("page_views", 1.0)) io.println("Metric sent to user actor! 📊") } Error(_) -> io.println("User actor not found") } } glixir.StartChildError(error) -> { io.println("Failed to start user actor: " <> error) } } }
GenServer Interop
import glixir import gleam/erlang/atom import gleam/dynamic import gleam/dynamic/decode // Example: Counter server (request type is atom, reply type is Int) pub fn main() { let assert Ok(server) = glixir.start_genserver("MyApp.Counter", dynamic.int(0)) let assert Ok(count) = glixir.call_genserver(server, atom.create("get_count"), decode.int) io.debug(count) // 0 let assert Ok(_) = glixir.cast_genserver(server, atom.create("increment")) let assert Ok(count) = glixir.call_genserver(server, atom.create("get_count"), decode.int) io.debug(count) // 1 }
Type Safety Notes
While this library provides type-safe wrappers on the Gleam side, remember:
- Runtime Types: Elixir/Erlang processes are dynamically typed at runtime
- Message Contracts: Ensure message formats match between Gleam and Elixir
- JSON Serialization: PubSub uses JSON for cross-process type safety
- Decoder Functions: Always provide appropriate decoders for return values
- Error Handling: Handle all potential decode and process errors
- Testing: Test integration points thoroughly
Real-World Use Cases
TrackTags - Auto-scaling metrics platform built with glixir:
- ✅ Dynamic user actors spawned per session
- ✅ Registry-based actor discovery by user ID
- ✅ Real-time metric collection via type-safe PubSub
- ✅ Fault-tolerant supervision trees
About the Author
Built by Rahmi Pruitt - Ex-Twitch/Amazon Engineer turned indie hacker, on a mission to bring Gleam to the mainstream! 🚀
"Making concurrent programming delightful, one type at a time."
⭐ Star this repo if glixir helped you build something awesome! Your support helps bring mature OTP tooling to the Gleam ecosystem.