Haxe -> Elixir compiler for the BEAM ecosystem, with first-class Phoenix/LiveView support. Write application code in Haxe and compile to conventional Elixir shapes for pure Elixir/OTP services and Phoenix applications.
Warning
Stability: the project is currently pre-1.0 (v0.x) and actively evolving.
Some features remain experimental/opt-in (for example source mapping, migrations .exs emission, fast_boot).
See Known Limitations and Versioning & Stability.
Reflaxe.Elixir is for teams that want standard Elixir/OTP runtime behavior, while authoring with stronger compile-time feedback.
- Keep standard Elixir runtime semantics: generated code follows normal module/function/tuple/map conventions.
- Add a typed authoring layer: catch shape mismatches (assigns, params, tagged results) before runtime.
- Improve large refactors: typed Haxe APIs and compiler checks help keep changes coherent across modules.
- Build ergonomic abstractions: Haxe macros/typing can encode reusable authoring patterns without changing your BEAM deployment model.
- Use it with or without Phoenix: works for pure Elixir/OTP codebases and Phoenix apps.
Elixir's failure model is still the foundation (supervision, process isolation, let-it-crash where appropriate). The typed layer helps you decide more deliberately what should crash, what should return data, and where boundaries should be explicit.
These abstractions should earn their place. The question is not "can Haxe do this?", but "does this reduce drift, duplication, or unsafe boundaries compared to direct Haxe->Elixir authoring without this extra layer?"
| Haxe authoring surface | Direct Haxe->Elixir baseline | Edge over that baseline | Example |
|---|---|---|---|
Module-level final routes = [...] |
Hand-maintained route/controller wiring in direct modules | Typed route declarations reduce path/action drift during refactors | examples/09-phoenix-router |
@:schema + @:changeset |
Manually keeping schema/changeset field surfaces aligned | Typed field/params surfaces catch boundary mismatches earlier | examples/06-user-management |
TypedQueryLambda |
Ad-hoc query composition with repeated field assumptions | Typed query lambdas keep predicates aligned with source model shapes | examples/todo-app |
@:protocol / @:impl / @:behaviour |
Repeated contract maintenance across implementation modules | One typed contract surface, multiple implementations, less signature drift | examples/14-abstraction-lab |
Typed wrappers over Elixir externs (for example elixir.Kernel) |
Repeated low-level guard/send/type-check boilerplate | Centralized boundary helpers with explicit typed call surfaces | examples/14-abstraction-lab |
For a focused walkthrough of these patterns in one place, see examples/14-abstraction-lab.
Tradeoff: you add a compile step and should still read generated Elixir for hot paths and debugging. If an abstraction does not remove real duplication or drift, direct Haxe->Elixir modules are usually the simpler choice.
Gleam is a strong typed BEAM language with its own language/runtime story. Reflaxe.Elixir takes a different approach:
- You author in Haxe and compile to Elixir.
- You integrate directly with Phoenix/LiveView/Ecto through typed extern surfaces.
- You can reuse Haxe tooling/macros and keep cross-target options where they make sense.
- Phoenix integration (LiveView/controllers/templates/routers) for documented paths
- HEEx-oriented template authoring modes (
tsx,balanced,metal) - Ecto schemas/changesets/typed query surfaces
- OTP patterns (GenServer/Supervisor/Registry)
- Mix integration (
mix compile.haxe, watcher workflows) and client hook builds with Genes
- Source mapping (
.ex.map,mix haxe.source_map) - Migration
.exsemission fast_boot
For exact boundaries, use:
If you're new to this stack, begin with:
Install with Lix (recommended)
npx lix scope create
# Install latest GitHub release tag
REFLAXE_ELIXIR_TAG="$(curl -fsSL https://api.github.com/repos/fullofcaffeine/reflaxe.elixir/releases/latest | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
npx lix install "github:fullofcaffeine/reflaxe.elixir#${REFLAXE_ELIXIR_TAG}"
# Download project-pinned Haxe deps
npx lix download-lib reflaxe.elixir
-cp src_haxe
-main my_app_hx.Main
-D reflaxe_runtime
-D no-utf16
-D elixir_output=lib/my_app_hx
-D app_name=MyApp
-dce fullImportant compiler flag note:
- Do not use
-D analyzer-optimizewhen targeting Elixir. - See Compiler Flags Guide.
Use the guided flow:
For gradual adoption in an existing Phoenix codebase:
Start from the Mix-based examples and author regular Elixir modules in Haxe:
npm run qa:sentinel
scripts/qa-logpeek.sh --run-id <RUN_ID> --until-done 120Haxe:
import elixir.types.Term;
import phoenix.Phoenix.HandleEventResult;
import phoenix.Phoenix.MountResult;
import phoenix.Phoenix.Socket;
typedef CounterAssigns = { count: Int };
@:native("MyAppWeb.CounterLive")
@:liveview
class CounterLive {
public static function mount(params: Term, session: Term, socket: Socket<CounterAssigns>): MountResult<CounterAssigns> {
return Ok(socket.assign(_.count, 0));
}
}Notes:
socketin LiveView callbacks isSocket<TAssigns>(the Phoenix callback shape), and you can call assign helpers on it directly (socket.assign(...)).LiveSocket<TAssigns>is still available as an explicit wrapper when you prefer pipe-style chaining or helper signatures that useLiveSocket._.countcan look odd at first because_usually means “unused variable.” Here,_is a macro marker.- Why the API looks like this: Haxe has no built-in “field reference literal” for typedef fields, so
LiveSocket.assignuses_.fieldas a compact compile-time selector. - What you get from it: the compiler validates the field name, converts to Phoenix atom style, and emits
assign(socket, :count, value)._does not exist at runtime. - Phoenix-style bulk assigns are available as
assign({ ... }), which emitsassign(socket, %{...}). - Typed-key APIs (
assignKey/assignNewKey/updateKey) are optional advanced mode withvar keys = phoenix.AssignKeys.of(MyAssigns). Use defaultassign(_.field, value)/assign({ ... })for shortest code; use typed keys when you want explicit key tokens in APIs. Tiny comparison:var keys = phoenix.AssignKeys.of(CounterAssigns);return Ok(socket.assignKey(keys.count, 0)); - In Haxe, write callback args naturally (
params,session). If they are unused, generated Elixir is automatically normalized to_params,_session. - API deep dive:
docs/04-api-reference/LIVE_SOCKET_ASSIGN_API.md
Generated Elixir shape:
defmodule CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
endMore examples:
Start at docs/README.md.
- Installation
- Writing Idiomatic Haxe for Elixir
- Elixir Idioms & Hygiene
- Haxe->Elixir Mappings
- Interop With Existing Elixir
- Phoenix Integration
- API Index
- LiveSocket Assign API
- Mix Tasks
- Elixir Injection Guide
- Troubleshooting
GPL-3.0 - see LICENSE.
