FIX.Session

An initiator-side FIX session engine for Elixir.

FIX.Session uses :gen_statem to manage connection lifecycle, FIX sequence numbers, logon/logout, heartbeats, gap recovery, persistence, and delivery of validated application messages. FIX 4.4 over TCP is the default configuration.

Status: Early development (0.1.0). The API and storage format may change. See Current limitations before using this library in production.

Features

Installation

Add fix_session to your dependencies in mix.exs:

def deps do
[
{:fix_session, "~> 0.1.0"}
]
end

Then fetch the dependency with mix deps.get.

The project requires Elixir ~> 1.19.

Getting started

A session does not start or own its store. The store must start first and outlive every session that uses it. Supervise the store and session with :rest_for_one so a store failure also restarts dependent sessions:

defmodule MyApp.FIXSupervisor do
use Supervisor
@impl true
def start_link(options) do
Supervisor.start_link(__MODULE__, options, name: __MODULE__)
end
@impl true
def init(_options) do
config =
FIX.Session.Config.new!(
session_id: :primary,
name: MyApp.FIXSession,
host: "fix.example.com",
port: 4001,
sender_comp_id: "SENDER",
target_comp_id: "TARGET",
app: MyApp.FIXHandler
)
children = [
{FIX.Session.Store.ETS, file: "/var/lib/my_app/fix_session.ets"},
{FIX.Session, config}
]
Supervisor.init(children, strategy: :rest_for_one)
end
end

With its default options, FIX.Session.Store.ETS creates the named table used by the default store_ref, so no additional store configuration is necessary.

The session connects immediately, sends Logon, and reconnects every five seconds after an ordinary connection failure. A session is available for application messages only after the counterparty's Logon has been accepted.

Receiving application messages

Set :app to a module that exports handle_message/2. The callback runs synchronously in the session process and receives the validated FIX.Message and session config:

defmodule MyApp.FIXHandler do
def handle_message(%FIX.Message{} = message, %FIX.Session.Config{} = config) do
send(MyApp.OrderManager, {:fix_message, config.session_id, message})
:ok
end
end

Keep this callback fast and non-blocking. If no :app module is configured, validated application messages are discarded.

Sending application messages

Pass a FIX.Message to the running session:

message = %FIX.Message{
msg_type: "D",
body: [
# Application fields for NewOrderSingle
]
}
:ok = FIX.Session.send_message(MyApp.FIXSession, message)

The session assigns and overwrites session-controlled fields, including BeginString(8), MsgSeqNum(34), SenderCompID(49), SendingTime(52), and TargetCompID(56). It persists the encoded message and advances next_out before writing to the socket.

Calling send_message/2 before Logon completes returns:

{:error, {:not_logged_on, current_state}}

Logging out

:ok = FIX.Session.logout(MyApp.FIXSession, "Application shutdown")

An operator-requested logout is terminal for that process: after the Logout exchange or timeout, it remains disconnected rather than reconnecting automatically. Restart the supervised session to establish a new connection.

Configuration

Create a validated %FIX.Session.Config{} with FIX.Session.Config.new!/1.

Required options

OptionDescription
:session_idLogical session identifier used as the store key
:hostCounterparty hostname, charlist, or IP tuple
:portCounterparty TCP port
:sender_comp_idLocal SenderCompID(49)
:target_comp_idCounterparty TargetCompID(56)

Common optional settings

OptionDefaultDescription
:namenilLocal or registry name passed to :gen_statem
:begin_string"FIX.4.4"FIX BeginString
:heartbeat_interval30HeartBtInt(108) in seconds; 0 disables liveness timers
:connect_timeout10_000Connection timeout in milliseconds
:logon_timeout10_000Time to wait for the peer's Logon, in milliseconds
:logout_timeout10_000Time to wait for Logout completion, in milliseconds
:reconnect_interval5_000Delay between connection attempts, in milliseconds
:transport_options[]Options for the configured transport (:gen_tcp or :ssl)
:dictionaryFIX.Dictionary.FIX44Dictionary used to parse inbound messages
:appnilApplication message callback module
:default_appl_ver_idnilOptional DefaultApplVerID(1137) included in Logon

Advanced integrations can replace :transport, :store, and :store_ref with modules and references implementing FIX.Session.Transport and FIX.Session.Store.

TLS

FIX.Session.Transport.TLS connects over :ssl with peer verification against the OS trust store by default:

config =
FIX.Session.Config.new!(
# ...
transport: FIX.Session.Transport.TLS,
transport_options: [cacertfile: "/etc/fix/counterparty_ca.pem"]
)

Pass :cacertfile or :cacerts to pin a counterparty CA (common with self-signed FIX endpoints), or verify: :verify_none to disable verification entirely. See the FIX.Session.Transport.TLS docs for the full list of defaults and how to override them.

Persistence

FIX.Session.Store.ETS is the default store. Its table is owned by a dedicated supervised process, so sequence numbers and outbound messages survive a session-process crash or reconnect.

The ETS store is not fully crash durable:

Use the ETS snapshot only when those guarantees are acceptable. Production deployments requiring crash durability should use FIX.Session.Store.EKV or FIX.Session.Store.Postgres (below), or implement FIX.Session.Store with transactional durable storage. FIX.Session.Store.Memory is intended for tests and development.

For isolated ETS stores, start the owner with name: nil, retrieve its table with FIX.Session.Store.ETS.table/1, and pass that table as :store_ref in the session config.

Durable storage with EKV

FIX.Session.Store.EKV persists sequence numbers and outbound messages to SQLite through EKV, so state survives store and VM crashes. It requires the optional :ekv dependency (which ships a vendored, precompiled SQLite NIF):

{:ekv, "~> 0.4"}

Supervise it in place of the ETS store and select it in the session config:

children = [
{FIX.Session.Store.EKV, data_dir: "/var/lib/my_app/fix_session"},
{FIX.Session, FIX.Session.Config.new!(store: FIX.Session.Store.EKV, ...)}
]

Reusing the same :data_dir across restarts resumes the persisted session state. See the FIX.Session.Store.EKV docs for options and durability details.

Durable storage with Postgres

FIX.Session.Store.Postgres persists sequence numbers and outbound messages through the host application's Ecto.Repo, committing each outbound message and its advanced sequence number in a single database transaction. It requires the optional :ecto_sql and :postgrex dependencies:

{:ecto_sql, "~> 3.10"},
{:postgrex, "~> 0.19"}

Create the tables from a migration in your application:

defmodule MyApp.Repo.Migrations.AddFixSessionTables do
use Ecto.Migration
def up, do: FIX.Session.Store.Postgres.Migrations.up()
def down, do: FIX.Session.Store.Postgres.Migrations.down()
end

The store owns no processes — pass your repo as the store_ref and supervise it before the session:

children = [
MyApp.Repo,
{FIX.Session,
FIX.Session.Config.new!(
store: FIX.Session.Store.Postgres,
store_ref: MyApp.Repo,
# ...
)}
]

Current limitations

Development

Fetch dependencies and run the test suite:

mix deps.get
mix test

The FIX.Session.Store.Postgres tests need a reachable Postgres server (FIX_SESSION_PG_URL, defaulting to postgres://postgres:postgres@localhost:5432/fix_session_test); without one they are skipped with a warning.

Format and compile with warnings treated as errors:

mix format --check-formatted
mix compile --warnings-as-errors

License

Licensed under the Apache License 2.0. See LICENSE.