Middleware

Middleware is reusable, server-side logic that runs before a page renders or a command executes. It is a natural home for cross-cutting concerns such as authentication, authorization, request enrichment (locale, theme, tenant, feature flags), rate limiting, or audit logging.

Middleware is server-only. Actions run on the client and cross no trust boundary, so they have no middleware - anything privileged an action needs goes through a command, which middleware gates server-side.

Defining middleware

Middleware lives in one of two places: an inline function on the page or component itself, or a module you reuse across pages and components. Inline suits one-off logic specific to a single page or component. A module suits anything shared, and is written as a leaf (does the work itself) or a composite (combines other middleware). The Server helpers (put_status, put_redirect, put_session, put_stash, and the rest) are available unqualified in all of them.

Inline middleware

The simplest middleware is a function defined right on the page or component, attached by name with middleware :name. It receives the Server struct and any options it was attached with ([] when none), and returns a Server struct - the same one, or one it has updated, which threads into the next middleware.

defmodule MyApp.DashboardPage do
  use Hologram.Page

  route "/dashboard"

  middleware :put_locale

  def put_locale(server, _opts) do
    put_stash(server, :locale, get_request_header(server, "accept-language"))
  end
end

Options passed in the declaration arrive as the second argument:

middleware :put_theme, default: "dark"

def put_theme(server, opts) do
  put_stash(server, :theme, get_cookie(server, "theme") || opts[:default])
end

Leaf middleware

A leaf is the reusable form: a module that defines call/2, with the same (server, opts) signature as an inline function but in its own module so multiple pages or components can share it.

defmodule MyApp.Middleware.RequireAuth do
  use Hologram.Middleware

  @impl Hologram.Middleware
  def call(server, _opts) do
    require_auth(server, get_session(server, :user_id))
  end

  defp require_auth(server, nil), do: put_redirect(server, MyApp.LoginPage)
  defp require_auth(server, _user_id), do: server
end

Composite middleware

A composite declares a sub-chain with the middleware macro instead of defining call/2 - the framework generates the call/2 that runs that sub-chain. Because a composite has a call/2 like any other middleware, it attaches anywhere a leaf can - including inside another composite. A terminal response from any member stops the rest, and that short-circuit carries through nested composites.

defmodule MyApp.Middleware.AdminGate do
  use Hologram.Middleware

  middleware MyApp.Middleware.RequireAuth
  middleware MyApp.Middleware.RequireAdmin
end

Attaching middleware

Attach middleware to a page or component with the middleware macro. The target is either a module or an inline :function name, and each takes optional keyword options:

Attach as many as you like, mixing inline functions and modules. Declarations run top to bottom, in the order written.

Composition

Composition is explicit. Middleware composes two ways: a base module that shares its declarations with every page or component that uses it, and a composite that bundles middleware into a single unit (shown above). Both resolve at compile time into one flat chain, with no runtime registry and nothing injected behind your back, so the full chain stays visible where it is attached - good for review.

A base module whose __using__ injects middleware declarations passes them on to every page or component that uses it - the base can wrap use Hologram.Page or use Hologram.Component. The child's own declarations accumulate after the inherited ones (parent first, then child), so a shared set plus a child-specific addition is just one more middleware line.

defmodule MyApp.AdminPage do
  defmacro __using__(_opts) do
    quote do
      use Hologram.Page

      middleware MyApp.Middleware.RequireAuth

      layout MyApp.AdminLayout
    end
  end
end
defmodule MyApp.SettingsPage do
  use MyApp.AdminPage

  route "/admin/settings"

  middleware MyApp.Middleware.RequireAdmin

  # ...
end

App-wide concerns are assembled per page or component - a shared base module or composite plugged into the ones that need it. This avoids the anti-pattern of a global gate wrongly applying to public pages (login, signup) that need no auth. Truly-universal concerns that apply to every request - CSRF protection and telemetry, for example - are handled at the framework level, not via your middleware.

Dispatch and scoping

Where middleware runs depends on where it is attached:

Component middleware lets a distributable component gate its own commands regardless of where it is embedded.

Security - a page's middleware does not cover its components' commands. Dispatch is flat. It runs only the target module's middleware, never a tree of them. So when a command targets a component - even one nested deep inside a page you have gated - that component's middleware runs, but the page's and any ancestor component's do not. There is no inheritance up the nesting. To protect a component's commands, attach the gate to the component itself, directly or through a shared composite the page and component both use. Do not rely on the page's gate to cover them.

Ordering note: anything that must run even on a rejected request (audit logging especially) goes early in the chain, since a terminal response stops everything after it.

The Server struct

Middleware operates on a Server struct - the same struct init/3 and command/3 receive. It has three zones:

Identity, session, and cookies live here too. The functions for reading the request and writing each zone are in the Helper reference below. Enrichment is a common middleware job - the example here resolves the current user once and stashes it, so everything downstream can read it:

defmodule MyApp.Middleware.LoadCurrentUser do
  use Hologram.Middleware

  @impl Hologram.Middleware
  def call(server, _opts) do
    load_user(server, get_session(server, :user_id))
  end

  defp load_user(server, nil), do: server

  defp load_user(server, user_id) do
    put_stash(server, :current_user, Accounts.get_user(user_id))
  end
end

Terminal responses

Middleware stops the request by producing a response - that is, by setting a status on the server. put_status sets that field rather than halting on the spot - the middleware runs to completion, and once it returns with a status set, the rest of the chain and the handler are skipped, and the response is built from the status, headers, and body the chain accumulated. Decorations such as cookies and headers set before the stop still apply, so denying and setting a cookie both land.

There is no separate halt - the status is the signal, set as an integer status code or an atom alias. A redirect sets a status, so it is inherently terminal:

defmodule MyApp.Middleware.RequireAdmin do
  use Hologram.Middleware

  @impl Hologram.Middleware
  def call(server, _opts) do
    check_role(server, get_session(server, :role))
  end

  defp check_role(server, :admin), do: server
  defp check_role(server, _role), do: put_status(server, :forbidden)
end

Proxy and trust model

The request fields reflect what the client claimed or what an upstream proxy resolved - they are not authenticated.

Key conventions

The three keyed stores differ in key type, by lifetime:

Helper reference

These helpers are imported into pages, components, and use Hologram.Middleware modules, so you call them unqualified. The get_* readers take an optional final default, returned when the value is absent (otherwise nil).

Reading the request

Building the response

Identity

Session and cookies

Available here too, documented in full on their own pages:

Stash

Status aliases

Each status code and the atom alias put_status accepts for it. An unknown alias raises.

Code Alias
100:continue
101:switching_protocols
102:processing
103:early_hints
200:ok
201:created
202:accepted
203:non_authoritative_information
204:no_content
205:reset_content
206:partial_content
207:multi_status
208:already_reported
226:im_used
300:multiple_choices
301:moved_permanently
302:found
303:see_other
304:not_modified
305:use_proxy
307:temporary_redirect
308:permanent_redirect
400:bad_request
401:unauthorized
402:payment_required
403:forbidden
404:not_found
405:method_not_allowed
406:not_acceptable
407:proxy_authentication_required
408:request_timeout
409:conflict
410:gone
411:length_required
412:precondition_failed
413:payload_too_large
414:uri_too_long
415:unsupported_media_type
416:range_not_satisfiable
417:expectation_failed
418:im_a_teapot
421:misdirected_request
422:unprocessable_entity
423:locked
424:failed_dependency
425:too_early
426:upgrade_required
428:precondition_required
429:too_many_requests
431:request_header_fields_too_large
451:unavailable_for_legal_reasons
500:internal_server_error
501:not_implemented
502:bad_gateway
503:service_unavailable
504:gateway_timeout
505:http_version_not_supported
506:variant_also_negotiates
507:insufficient_storage
508:loop_detected
510:not_extended
511:network_authentication_required
Sponsored by
Curiosum
Main sponsor
Erlang Ecosystem Foundation
Milestone sponsor
Previous
← Context