Sessions

Persist state on the client between commands

Each command runs and exits, so anything you want to remember between commands (who’s logged in, a chosen project) has to live on the client. Terminalwire gives every server a small storage area on the user’s workstation, the same place a browser keeps cookies. You write to it with the file API.

Unlike Rails, the Elixir server doesn’t ship a session helper. You don’t need much: sign a value so the user can’t forge it, write it to a file, read it back. Phoenix.Token does the signing with your endpoint’s secret.

A session, in three functions

defmodule MyApp.CLI do
  use Terminalwire.CLI, name: "my-app"
  alias Terminalwire.Server.Context

  @session "session"
  @salt "user auth"
  @max_age 60 * 60 * 24 * 30  # 30 days

  defp put_current_user(user) do
    token = Phoenix.Token.sign(MyAppWeb.Endpoint, @salt, user.id)
    Context.file_write(context(), @session, token)
  end

  defp current_user do
    ctx = context()

    with true <- Context.file_exists?(ctx, @session),
         token <- Context.file_read(ctx, @session),
         {:ok, id} <- Phoenix.Token.verify(MyAppWeb.Endpoint, @salt, token, max_age: @max_age) do
      MyApp.Accounts.get_user(id)
    else
      _ -> nil
    end
  end

  defp log_out, do: Context.file_delete(context(), @session)
end

put_current_user/1 signs the user’s id and writes it; current_user/0 reads the file and verifies the signature, returning nil if it’s missing, tampered with, or past max_age; log_out/0 deletes the file. Because the token is signed with secret_key_base, the user can read the file but can’t change who they are.

Use it in commands

@desc "Show who you're logged in as"
def whoami do
  case current_user() do
    nil -> puts("Not logged in. Run: my-app login")
    user -> puts("Logged in as #{user.email}")
  end
end

Authentication builds login on top of these helpers, with both a password prompt and a browser flow.

Where it lives

The file sits in your server’s storage directory on the client, alongside any other state you write. It’s scoped to your server’s origin, so one app’s session can’t be read by another. Keep what you store small; every read and write is a round trip to the workstation.