# Elixir Part 3: Phoenix Framework — v1.8.5 (Current) Phoenix is the web framework for Elixir. Version 1.8.5 is current. It provides MVC controllers, real-time LiveView, Channels for WebSocket communication, and comprehensive tooling for authentication, testing, and deployment. ## What's New in Phoenix 1.8 - **Scopes** in generators — secure data access by default (e.g., `current_user` automatically applied) - **Magic links** (passwordless auth) and **"sudo mode"** in `mix phx.gen.auth` - **daisyUI** integration — light/dark/system mode support out of the box - **Simplified layouts** — single `root.html.heex` wraps everything; dynamic layouts are function components - **`use Phoenix.Controller` now requires `:formats`** — must specify `formats: [:html]` or `formats: [:html, :json]` - **Updated security headers** — `content-security-policy` with `base-uri 'self'; frame-ancestors 'self'`; dropped deprecated `x-frame-options` and `x-download-options` - **`config` variable removed** from `Phoenix.Endpoint` — use `Application.compile_env/3` instead - **Deprecated**: `:namespace`, `:put_default_views`, layouts without modules, `:trailing_slash` in router --- ## Project Setup ```bash # New project (Phoenix Express — quickest path) curl https://new.phoenixframework.org/my_app | sh # Traditional setup mix phx.new my_app mix phx.new my_app --no-ecto # Without database mix phx.new my_app --no-html # API only mix phx.new my_app --database sqlite3 # SQLite instead of Postgres ``` ## Directory Structure ``` my_app/ ├── lib/ │ ├── my_app/ # Business logic (contexts, schemas) │ │ ├── application.ex # Supervision tree │ │ ├── repo.ex # Ecto Repo │ │ └── catalog/ # Context: Catalog │ │ ├── product.ex # Schema │ │ └── catalog.ex # Context module │ └── my_app_web/ # Web layer │ ├── endpoint.ex # HTTP entry point │ ├── router.ex # Routes + pipelines │ ├── components/ │ │ ├── core_components.ex # Shared UI components │ │ └── layouts.ex # Layout components │ ├── controllers/ │ │ ├── page_controller.ex │ │ └── page_html/ # Templates for controller │ │ └── home.html.heex │ └── live/ # LiveView modules │ └── counter_live.ex ├── config/ │ ├── config.exs # Compile-time config │ ├── dev.exs / prod.exs │ └── runtime.exs # Runtime config (env vars, secrets) ├── priv/ │ ├── repo/migrations/ │ └── static/ # Static assets ├── test/ │ ├── support/ │ │ ├── conn_case.ex # Test helpers for controllers │ │ └── data_case.ex # Test helpers for data layer │ └── my_app_web/ └── mix.exs ``` --- ## Router & Pipelines ```elixir defmodule MyAppWeb.Router do use MyAppWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {MyAppWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", MyAppWeb do pipe_through :browser get "/", PageController, :home live "/counter", CounterLive # LiveView route resources "/products", ProductController # RESTful CRUD end scope "/api", MyAppWeb do pipe_through :api resources "/items", ItemController, except: [:new, :edit] end end ``` --- ## Verified Routes — `~p` Sigil **Always use `~p` instead of string paths.** Compile-time verified against your router. ```elixir # In templates (HEEx) ~H""" View Edit """ # In controllers redirect(conn, to: ~p"/products/#{product}") # With query params ~p"/products?page=#{page}&sort=name" ~p"/products?#{%{page: 1, sort: "name"}}" # URL generation (includes host) url(~p"/products/#{product}") # => "https://example.com/products/42" ``` --- ## Controllers (1.8 Style) ```elixir defmodule MyAppWeb.ProductController do use MyAppWeb, :controller # REQUIRED in 1.8: specify formats # This is typically set in MyAppWeb :controller function def index(conn, _params) do products = Catalog.list_products() render(conn, :index, products: products) end def show(conn, %{"id" => id}) do product = Catalog.get_product!(id) render(conn, :show, product: product) end def create(conn, %{"product" => product_params}) do case Catalog.create_product(product_params) do {:ok, product} -> conn |> put_flash(:info, "Product created.") |> redirect(to: ~p"/products/#{product}") {:error, %Ecto.Changeset{} = changeset} -> render(conn, :new, changeset: changeset) end end end ``` **View module naming:** For `ProductController`, Phoenix looks for `ProductHTML` (for `:html` format) and `ProductJSON` (for `:json` format). ```elixir # lib/my_app_web/controllers/product_html.ex defmodule MyAppWeb.ProductHTML do use MyAppWeb, :html embed_templates "product_html/*" # Loads .heex files from directory end # lib/my_app_web/controllers/product_json.ex defmodule MyAppWeb.ProductJSON do def index(%{products: products}), do: %{data: for(p <- products, do: data(p))} def show(%{product: product}), do: %{data: data(product)} defp data(product), do: %{id: product.id, title: product.title, price: product.price} end ``` --- ## Components and HEEx ```elixir defmodule MyAppWeb.CoreComponents do use Phoenix.Component # Declare attributes with types and docs attr :type, :string, default: "button" attr :class, :string, default: nil attr :rest, :global # Passes through all other HTML attrs slot :inner_block, required: true def button(assigns) do ~H""" """ end # Table component with slots attr :rows, :list, required: true slot :col, required: true do attr :label, :string end def table(assigns) do ~H"""
{col[:label]}
{render_slot(col, row)}
""" end end ``` **HEEx syntax notes:** - `{@var}` — render assign (curly braces, not `<%= %>`) - `:if={condition}` — conditional rendering on any tag - `:for={item <- list}` — iteration on any tag - `<.component_name />` — call function component with dot notation - `` — call remote component with module name - `{render_slot(@inner_block)}` — render slot content - `<:slot_name>content` — named slot content --- ## LiveView ```elixir defmodule MyAppWeb.SearchLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, query: "", results: [])} end def handle_params(%{"q" => query}, _uri, socket) do {:noreply, assign(socket, query: query, results: search(query))} end def handle_params(_params, _uri, socket), do: {:noreply, socket} def handle_event("search", %{"query" => query}, socket) do {:noreply, socket |> assign(query: query, results: search(query)) |> push_patch(to: ~p"/search?q=#{query}")} end def render(assigns) do ~H"""

{result.title}

{result.summary}

""" end defp search(query), do: MyApp.Search.find(query) end ``` **LiveView lifecycle:** `mount/3` → `handle_params/3` → `render/1`. Events via `handle_event/3`. Server pushes via `handle_info/2`. **Key patterns:** - `assign/2,3` — set socket assigns - `push_navigate/2` — navigate to new LiveView (full mount) - `push_patch/2` — update URL without full remount (calls `handle_params`) - `push_event/3` — push event to client JS hooks - `stream/3,4` — efficient list rendering for large collections (inserts/deletes without re-rendering entire list) - `async_assign/3` + `assign_async/3` — async data loading with loading/error states --- ## Channels — Real-Time WebSocket ```elixir # In endpoint.ex socket "/socket", MyAppWeb.UserSocket, websocket: true, longpoll: false # lib/my_app_web/channels/user_socket.ex defmodule MyAppWeb.UserSocket do use Phoenix.Socket channel "room:*", MyAppWeb.RoomChannel def connect(%{"token" => token}, socket, _connect_info) do case Phoenix.Token.verify(socket, "user auth", token, max_age: 86400) do {:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)} {:error, _} -> :error end end def id(socket), do: "users_socket:#{socket.assigns.user_id}" end # lib/my_app_web/channels/room_channel.ex defmodule MyAppWeb.RoomChannel do use MyAppWeb, :channel def join("room:" <> room_id, _params, socket) do {:ok, assign(socket, :room_id, room_id)} end def handle_in("new_msg", %{"body" => body}, socket) do broadcast!(socket, "new_msg", %{ body: body, user_id: socket.assigns.user_id }) {:noreply, socket} end end ``` **Force disconnect all sessions for a user:** ```elixir MyAppWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) ``` --- ## Contexts — Business Logic Boundary Contexts are plain Elixir modules that encapsulate data access and business rules. They are the API between your web layer and your domain. ```elixir # Generate with: mix phx.gen.context Catalog Product products title:string price:decimal defmodule MyApp.Catalog do import Ecto.Query alias MyApp.Repo alias MyApp.Catalog.Product def list_products do Repo.all(Product) end def get_product!(id), do: Repo.get!(Product, id) def create_product(attrs \\ %{}) do %Product{} |> Product.changeset(attrs) |> Repo.insert() end def update_product(%Product{} = product, attrs) do product |> Product.changeset(attrs) |> Repo.update() end def delete_product(%Product{} = product) do Repo.delete(product) end def change_product(%Product{} = product, attrs \\ %{}) do Product.changeset(product, attrs) end end ``` **Context design principles:** - One context per bounded domain (Catalog, Accounts, Orders) - Contexts own their schemas — other contexts reference by ID, not struct - Cross-context calls go through the public context API, never access another context's Repo directly - Contexts can nest related schemas (Comments under Posts) --- ## Authentication — `mix phx.gen.auth` ```bash mix phx.gen.auth Accounts User users ``` Phoenix 1.8 generates: - **Magic links** (passwordless) — email-based login links - **"Sudo mode"** — re-authentication for sensitive actions - Session-based auth with secure token handling - Email confirmation and password reset flows - `require_authenticated_user` plug for protected routes ## Scopes (New in 1.8) Scopes make secure data access the default in generators. When you generate resources with a scope, all queries are automatically filtered by the scoped user. ```bash mix phx.gen.live Posts Post posts title body:text --scope current_user ``` This generates code that automatically passes `current_user` to context functions, ensuring users only see their own data. --- ## Ecto — Database Layer Ecto has four core components: **Repo** (database wrapper), **Schema** (data mapping), **Changeset** (validation + change tracking), and **Query** (composable queries). Always preload associations explicitly — Ecto never lazy-loads. ### Schema ```elixir defmodule MyApp.Catalog.Product do use Ecto.Schema import Ecto.Changeset schema "products" do field :title, :string field :price, :decimal field :status, Ecto.Enum, values: [:draft, :published, :archived] field :metadata, :map, default: %{} field :tags, {:array, :string}, default: [] field :computed, :string, virtual: true # Not persisted field :slug, :string, source: :url_slug # Maps to different DB column has_many :reviews, MyApp.Reviews.Review has_one :detail, MyApp.Catalog.ProductDetail belongs_to :category, MyApp.Catalog.Category many_to_many :tags, MyApp.Tag, join_through: "product_tags" embeds_one :seo, SEO, on_replace: :update do field :meta_title, :string field :meta_description, :string end embeds_many :variants, Variant, on_replace: :delete do field :sku, :string field :price, :decimal end timestamps(type: :utc_datetime) end def changeset(product, attrs) do product |> cast(attrs, [:title, :price, :status, :category_id, :metadata, :tags]) |> cast_embed(:seo) |> cast_embed(:variants) |> validate_required([:title, :price]) |> validate_number(:price, greater_than: 0) |> validate_length(:title, min: 3, max: 255) |> validate_inclusion(:status, [:draft, :published, :archived]) |> unique_constraint(:title) |> foreign_key_constraint(:category_id) end end ``` **Field types:** `:string`, `:integer`, `:float`, `:decimal`, `:boolean`, `:binary`, `:map`, `{:array, type}`, `:date`, `:time`, `:naive_datetime`, `:utc_datetime`, `:utc_datetime_usec`, `:binary_id`, `Ecto.UUID`, `Ecto.Enum` **Field options:** `:default`, `:source` (DB column name), `:virtual` (not persisted), `:redact` (mask in inspect), `:read_after_writes` (re-read from DB post-write), `:autogenerate` **Embed `:on_replace` options:** `:raise` (default), `:mark_as_invalid`, `:update` (embeds_one), `:delete` (embeds_many) ### Changeset — Validation & Change Tracking ```elixir # Validations (run in-memory, no DB) |> validate_required([:field]) |> validate_format(:email, ~r/@/) |> validate_length(:name, min: 2, max: 100) |> validate_number(:age, greater_than: 0, less_than: 150) |> validate_inclusion(:role, [:admin, :user]) |> validate_exclusion(:username, ["admin", "root"]) |> validate_acceptance(:terms) # Checkbox must be true |> validate_confirmation(:password) # password_confirmation must match |> validate_subset(:permissions, [:read, :write, :admin]) |> validate_change(:email, fn :email, email -> # Custom per-field validation if String.contains?(email, "+"), do: [email: "no plus addressing"], else: [] end) # Constraints (checked by DB — only run if validations pass) |> unique_constraint(:email) |> unique_constraint([:user_id, :project_id], name: :user_projects_unique_index) |> foreign_key_constraint(:category_id) |> check_constraint(:price, name: :price_must_be_positive) |> no_assoc_constraint(:reviews) # Prevent delete if has associations |> exclusion_constraint(:date_range) # Postgres range exclusion ``` **Schemaless changesets** — validate arbitrary data without a schema: ```elixir types = %{email: :string, age: :integer} changeset = {%{}, types} |> Ecto.Changeset.cast(params, Map.keys(types)) |> Ecto.Changeset.validate_required([:email]) case Ecto.Changeset.apply_action(changeset, :validate) do {:ok, data} -> use_data(data) {:error, changeset} -> show_errors(changeset) end ``` **Error traversal:** ```elixir Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Regex.replace(~r"%{(\w+)}", msg, fn _, key -> opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() end) end) # => %{title: ["can't be blank"], price: ["must be greater than 0"]} ``` ### Query — Composable & Dynamic ```elixir import Ecto.Query # Composable queries — chain them together def published(query \\ Product) do from p in query, where: p.status == :published end def recent(query, days \\ 7) do from p in query, where: p.inserted_at > ago(^days, "day") end def with_reviews(query) do from p in query, preload: [:reviews] end # Usage: Product |> published() |> recent(30) |> with_reviews() |> Repo.all() ``` **Named bindings** — track joins without positional counting: ```elixir from p in Post, as: :post, join: c in assoc(p, :comments), as: :comment, where: as(:comment).approved == true, select: {as(:post).title, count(as(:comment).id)}, group_by: as(:post).id ``` **Dynamic queries** — build conditions programmatically: ```elixir def filter(params) do query = from(p in Product) conditions = true conditions = if params["status"], do: dynamic([p], p.status == ^params["status"] and ^conditions), else: conditions conditions = if params["min_price"], do: dynamic([p], p.price >= ^params["min_price"] and ^conditions), else: conditions from p in query, where: ^conditions end ``` **Subqueries:** ```elixir top_products = from p in Product, order_by: [desc: :sales], limit: 10 from p in subquery(top_products), select: avg(p.price) ``` **Window functions:** ```elixir from e in Employee, select: {e.name, e.salary, over(avg(e.salary), :dept)}, windows: [dept: [partition_by: e.department_id, order_by: e.salary]] ``` **Fragments (raw SQL):** ```elixir from p in Post, where: fragment("? @> ?", p.tags, ^["elixir"]), # Postgres array contains order_by: fragment("random()") ``` **Preloading strategies:** ```elixir # Separate queries (default) — parallelized, no data duplication Repo.all(Post) |> Repo.preload([:comments, :author]) # Join preload — single query, watch for Cartesian product from p in Post, join: c in assoc(p, :comments), preload: [comments: c] # Custom query preload Repo.preload(posts, comments: from(c in Comment, order_by: c.inserted_at)) # Nested preload Repo.preload(posts, [comments: [:author, :replies]]) ``` **Important:** Use `is_nil(field)` not `field == nil` in queries. Only one `select` per query — use `select_merge` to compose. ### Repo — Database Operations ```elixir # CRUD Repo.insert(changeset) # {:ok, struct} | {:error, changeset} Repo.update(changeset) Repo.delete(struct) Repo.insert!(changeset) # Returns struct or raises Repo.get(Post, 1) # nil if not found Repo.get!(Post, 1) # Raises if not found Repo.get_by(Post, title: "Hello") # By arbitrary fields Repo.one(query) # Raises if > 1 result # Bulk operations — return {count, nil | results} Repo.insert_all(Post, [%{title: "A"}, %{title: "B"}]) Repo.update_all(from(p in Post, where: p.old == true), set: [archived: true]) Repo.delete_all(from(p in Post, where: p.inserted_at < ago(1, "year"))) # NOTE: update_all does NOT update auto-generated fields like updated_at # Upserts (on_conflict) Repo.insert(changeset, on_conflict: :nothing, # Ignore conflict conflict_target: [:email] ) Repo.insert(changeset, on_conflict: {:replace, [:name, :updated_at]}, # Update specific fields conflict_target: [:email] ) Repo.insert(changeset, on_conflict: :replace_all, # Replace everything conflict_target: :id ) # Aggregation Repo.aggregate(Post, :count) # SELECT count(*) Repo.aggregate(Post, :sum, :views) # SELECT sum(views) Repo.aggregate(query, :avg, :price) # Works with queries too ``` ### Ecto.Multi — Atomic Transactions Group operations that must all succeed or all fail: ```elixir alias Ecto.Multi Multi.new() |> Multi.insert(:post, Post.changeset(%Post{}, post_attrs)) |> Multi.insert(:comment, fn %{post: post} -> Comment.changeset(%Comment{}, %{post_id: post.id, body: "First!"}) end) |> Multi.update_all(:increment, from(u in User, where: u.id == ^user_id), inc: [post_count: 1]) |> Multi.run(:notify, fn _repo, %{post: post} -> # Arbitrary logic — return {:ok, _} or {:error, _} Notifications.send_new_post(post) end) |> Repo.transaction() # => {:ok, %{post: %Post{}, comment: %Comment{}, increment: {1, nil}, notify: :sent}} # => {:error, failed_op, failed_value, changes_so_far} ``` **Key patterns:** Each operation name must be unique. Failed operations roll back everything. Use `Multi.run/3` for arbitrary logic. Test with `Multi.to_list/1` to inspect without executing. ### Streaming — Large Result Sets ```elixir Repo.transaction(fn -> Post |> where([p], p.status == :published) |> Repo.stream(max_rows: 500) |> Stream.each(&process_post/1) |> Stream.run() end) ``` Must execute within a transaction. Default batch size is 500 rows. ### Migrations ```elixir defmodule MyApp.Repo.Migrations.CreateProducts do use Ecto.Migration def change do create table("products") do add :title, :string, null: false, size: 255 add :price, :decimal, precision: 10, scale: 2 add :status, :string, default: "draft" add :metadata, :map, default: %{} add :tags, {:array, :string}, default: [] add :category_id, references("categories", on_delete: :restrict) timestamps(type: :utc_datetime) end create index("products", [:category_id]) create unique_index("products", [:title]) create index("products", [:status, :inserted_at]) create index("products", [:tags], using: :gin) # Postgres array index create index("products", [:title], where: "status = 'published'", name: :published_title_idx) # Partial index end end ``` **Alter tables:** ```elixir def change do alter table("products") do add :slug, :string modify :title, :text, from: :string # :from required for reversibility remove :deprecated_field, :string # type required for reversibility end # Raw SQL when needed execute "CREATE EXTENSION IF NOT EXISTS \"pg_trgm\"", "DROP EXTENSION IF EXISTS \"pg_trgm\"" end ``` **Run migrations:** `mix ecto.migrate` / `mix ecto.rollback` / `mix ecto.reset` ### Ecto.Enum ```elixir # String storage (default) field :status, Ecto.Enum, values: [:draft, :published, :archived] # Integer storage field :priority, Ecto.Enum, values: [low: 1, medium: 2, high: 3] # Array of enums field :roles, {:array, Ecto.Enum}, values: [:admin, :editor, :viewer] # Query helpers Ecto.Enum.values(Product, :status) # [:draft, :published, :archived] Ecto.Enum.dump_values(Product, :status) # ["draft", "published", "archived"] Ecto.Enum.mappings(Product, :status) # [draft: "draft", ...] — for form dropdowns ``` --- ## Telemetry — Built-in Observability Phoenix 1.8 includes a Telemetry supervisor that tracks request duration, Ecto query times, and VM metrics out of the box. ```elixir # lib/my_app_web/telemetry.ex (auto-generated) defmodule MyAppWeb.Telemetry do use Supervisor import Telemetry.Metrics def metrics do [ summary("phoenix.endpoint.stop.duration", unit: {:native, :millisecond}), summary("phoenix.router_dispatch.stop.duration", tags: [:route], unit: {:native, :millisecond}), summary("my_app.repo.query.total_time", unit: {:native, :millisecond}), summary("vm.memory.total", unit: {:byte, :kilobyte}), summary("vm.total_run_queue_lengths.total"), summary("vm.total_run_queue_lengths.cpu"), ] end end ``` Integrates with **PromEx** for Prometheus/Grafana dashboards (see Part 4: Ecosystem). --- ## Security Best Practices **Never pass untrusted input to:** `Code.eval_string/3`, `:os.cmd/2`, `System.cmd/3`, `System.shell/2`, `:erlang.binary_to_term/2` **Ecto prevents SQL injection by default** — the query DSL parameterizes all inputs. Only `Ecto.Adapters.SQL.query/4` with raw string interpolation is vulnerable. **Safe deserialization:** ```elixir # UNSAFE — even with :safe flag :erlang.binary_to_term(user_input, [:safe]) # SAFE — prevents executable terms Plug.Crypto.non_executable_binary_to_term(user_input, [:safe]) ``` **CSRF protection** is built into the `:browser` pipeline via `protect_from_forgery`. **Content Security Policy** is set by `put_secure_browser_headers`. --- ## Testing Phoenix ```elixir # Controller test defmodule MyAppWeb.ProductControllerTest do use MyAppWeb.ConnCase test "GET /products", %{conn: conn} do conn = get(conn, ~p"/products") assert html_response(conn, 200) =~ "Products" end test "POST /products creates product", %{conn: conn} do conn = post(conn, ~p"/products", product: %{title: "Widget", price: 9.99}) assert redirected_to(conn) =~ ~p"/products/" end end # LiveView test defmodule MyAppWeb.CounterLiveTest do use MyAppWeb.ConnCase import Phoenix.LiveViewTest test "increments counter", %{conn: conn} do {:ok, view, html} = live(conn, ~p"/counter") assert html =~ "Count: 0" assert view |> element("button", "+1") |> render_click() =~ "Count: 1" end end # Channel test defmodule MyAppWeb.RoomChannelTest do use MyAppWeb.ChannelCase test "broadcasts new messages" do {:ok, _, socket} = subscribe_and_join(socket(MyAppWeb.UserSocket), MyAppWeb.RoomChannel, "room:lobby") push(socket, "new_msg", %{"body" => "hello"}) assert_broadcast "new_msg", %{body: "hello"} end end ``` --- ## Generators Cheat Sheet ```bash # HTML CRUD (controller + views + templates + context + schema + migration) mix phx.gen.html Catalog Product products title:string price:decimal # LiveView CRUD mix phx.gen.live Catalog Product products title:string price:decimal # JSON API mix phx.gen.json Catalog Product products title:string price:decimal # Context + schema only (no web layer) mix phx.gen.context Catalog Product products title:string price:decimal # Schema + migration only mix phx.gen.schema Product products title:string price:decimal # Authentication mix phx.gen.auth Accounts User users # Channel mix phx.gen.channel Room # Presence mix phx.gen.presence # Release files (Dockerfile, release.ex, overlay scripts) mix phx.gen.release mix phx.gen.release --docker # Include Dockerfile ``` --- ## Release & Deployment ```bash # Generate release infrastructure mix phx.gen.release --docker # Build release MIX_ENV=prod mix deps.get --only prod MIX_ENV=prod mix compile MIX_ENV=prod mix assets.deploy MIX_ENV=prod mix release # Release commands _build/prod/rel/my_app/bin/server # Start with Phoenix server _build/prod/rel/my_app/bin/migrate # Run migrations _build/prod/rel/my_app/bin/my_app remote # Attach IEx console ``` **Runtime config (`config/runtime.exs`):** ```elixir import Config if config_env() == :prod do database_url = System.fetch_env!("DATABASE_URL") secret_key_base = System.fetch_env!("SECRET_KEY_BASE") config :my_app, MyApp.Repo, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") config :my_app, MyAppWeb.Endpoint, url: [host: System.fetch_env!("PHX_HOST"), port: 443, scheme: "https"], http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")], secret_key_base: secret_key_base end ```