ExAtlas's ExAtlas.Fly.* namespace provides first-class Fly.io platform
operations — independent of the GPU-compute provider pipeline. If you're
already using atlas for compute, Fly ops ride alongside with no extra
dependencies; if you're only using atlas for Fly ops, ignore the compute API.
This guide covers: installation, configuration, token lifecycle, discovering apps, streaming logs, and streaming deploys.
Installation
The fastest path is the Igniter installer:
mix igniter.install ex_atlas
# or, if atlas is already a dep:
mix ex_atlas.install
That writes a sensible config :ex_atlas, :fly block, creates the DETS storage
directory, and wires phoenix_pubsub if your app uses Phoenix.
Manual install — add to mix.exs:
{:ex_atlas, "~> 0.2"}ExAtlas is a regular OTP application — its supervision tree starts automatically. The Fly sub-tree boots by default; disable with:
config :ex_atlas, :fly, enabled: falseConfiguration
All options live under config :ex_atlas, :fly:
config :ex_atlas, :fly,
# Master switch (default: true). Set false to skip the whole Fly sub-tree.
enabled: true,
# Token storage (default: ExAtlas.Fly.TokenStorage.Dets)
token_storage: ExAtlas.Fly.TokenStorage.Dets,
storage_path: "priv/ex_atlas_fly",
# Dispatcher mode (default: :registry)
dispatcher: :registry, # :registry | :phoenix_pubsub | {:mfa, {m,f,a}}
pubsub: MyApp.PubSub, # required when dispatcher: :phoenix_pubsub
# Log endpoint + poll interval
log_endpoint: "https://api.machines.dev/v1/apps",
poll_interval_ms: 2_000,
# Token resolution
fly_config_file_enabled: true, # read ~/.fly/config.yml when cache misses
cli_timeout_ms: 15_000 # `fly tokens create` timeoutDiscovering apps
discover_apps/1 scans fly.toml files at the project root and one level of
subdirectories (monorepo-friendly):
ExAtlas.Fly.discover_apps("/path/to/project")
# => [{"my-api", "/path/to/project"}, {"my-web", "/path/to/project/web"}]Tailing logs
Subscribe from any process (LiveView, GenServer, plain pid):
ExAtlas.Fly.subscribe_logs("my-api", "/path/to/project")
# In the subscriber:
def handle_info({:ex_atlas_fly_logs, "my-api", entries}, state) do
# entries :: [ExAtlas.Fly.Logs.LogEntry.t()]
...
endA single Streamer GenServer runs per app regardless of subscriber count.
When all subscribers disconnect, the streamer stops itself.
Streaming deploys
Subscribe to a per-ticket deploy topic, then launch the deploy:
ExAtlas.Fly.subscribe_deploy(ticket_id)
Task.start(fn ->
ExAtlas.Fly.stream_deploy(project_path, "web", ticket_id)
end)
# In the subscriber:
def handle_info({:ex_atlas_fly_deploy, ^ticket_id, line}, state) do
...
endThe streamer enforces two timeouts:
- Activity timer (5 min) — resets on each chunk of output. Catches hung builders.
- Absolute timer (30 min) — never resets. Caps total deploy time.
deploy/2 (non-streaming) is the simpler sync variant with a 15 min timeout
and the full output returned as a binary.
Token lifecycle
ExAtlas.Fly.Tokens resolves tokens with this chain:
- ETS — O(1) in-memory, 24 h TTL.
ExAtlas.Fly.TokenStorage— durable (DETS by default) so cached tokens survive restarts.~/.fly/config.yml— the fileflyctlwrites afterfly auth login.fly tokens create readonly— CLI fallback with a 15 s timeout.- Manual override — a token the host set via
ExAtlas.Fly.Tokens.set_manual/2.
Typical usage:
{:ok, token} = ExAtlas.Fly.Tokens.get("my-api")
# Force re-acquisition (e.g. after a 401):
ExAtlas.Fly.Tokens.invalidate("my-api")
# Store a user-supplied override:
ExAtlas.Fly.Tokens.set_manual("my-api", "fo1_...")ExAtlas.Fly.Logs.Client.fetch_logs_with_retry/2 already invalidates on 401 and
retries once automatically.
Pluggable token storage
For hosts that want tokens in a different store (a DB, a vault, etc.),
implement the ExAtlas.Fly.TokenStorage behaviour:
defmodule MyApp.FlyTokenStorage do
@behaviour ExAtlas.Fly.TokenStorage
def child_spec(_opts), do: %{id: __MODULE__, start: {__MODULE__, :start_link, []}}
def start_link, do: Agent.start_link(fn -> %{} end, name: __MODULE__)
def get(app, key), do: ...
def put(app, key, record), do: ...
def delete(app, key), do: ...
end
# config/config.exs
config :ex_atlas, :fly, token_storage: MyApp.FlyTokenStorageExAtlas will supervise your module in its Fly sub-tree.
Dispatcher modes
ExAtlas cannot hard-depend on Phoenix, so logs/deploys are dispatched through
ExAtlas.Fly.Dispatcher with three modes:
:registry(default) — atlas starts aRegistryand usessend/2. Zero-deps. Best for non-Phoenix hosts.:phoenix_pubsub— usesPhoenix.PubSub.broadcast/3. Requiresphoenix_pubsubin your deps andconfig :ex_atlas, :fly, pubsub: MyApp.PubSub. Best when you already have a cluster-wide PubSub.{:mfa, {Mod, :fun, extra_args}}— custom: on each dispatch atlas callsapply(Mod, :fun, [topic, message | extra_args]).
Subscriber message shapes are stable across modes.
Testing
For unit tests, swap in the in-memory token store:
defmodule MyTest do
use ExUnit.Case
setup do
start_supervised!(ExAtlas.Fly.TokenStorage.Memory)
Application.put_env(:ex_atlas, :fly, token_storage: ExAtlas.Fly.TokenStorage.Memory)
:ok
end
endFor HTTP-level tests, point ExAtlas.Fly.Logs.Client at a Bypass endpoint via
base_url:.