Documentation

A Host's Route Helpers Work Inside the Engine

Accepted Decided 2026-09-16 host-integration · routing · engine-isolation · authentication

Context

isolate_namespace Janela points every route helper evaluated inside Janela’s controllers at the engine’s own route set. That is the right default for the engine’s own code. It is the wrong answer for the host’s, and the host’s runs there constantly, because Janela::ApplicationController inherits the host’s ApplicationController on purpose (ADR 004).

Three reports, one cause, and each host found it separately:

  1. A host’s application layout calling root_path in its nav, which made every pane 500. Solved another way in 0.2.1 by not rendering panes in the host’s layout (ADR 011).
  2. A host authenticating per controller, whose before_action redirected to new_session_path.
  3. A host authenticating globally, whose Authentication concern redirected to new_session_path from inside the engine and raised ActionController::UrlGenerationError. An unauthenticated visitor got a 500 instead of a sign-in page.

Case 3 is the one that shows the shape of the problem. The README had framed the fix as something to do “if your app authenticates per controller”, which is not the trigger at all. The trigger is that a URL is generated by host code while it is inside the engine. A rescue_from that redirects, an after_action, a flash partial and an audit callback all qualify. Each host discovers it on its own, in production, as a 500.

main_app.new_session_path is the documented fix and it works. It also asks every host to know that Janela is an isolated engine, and to remember it in code that has nothing to do with Janela. That is the opposite of what installing this gem is supposed to feel like.

The option of including the host’s url_helpers wholesale was considered and rejected. Both sets of helpers would then be present, and a name in both would resolve by module order, so frames_path inside Janela’s own view could silently become the host’s /frames. Trading a loud UrlGenerationError for a quietly wrong link is a bad trade.

Decision

Janela forwards to the host exactly those route helpers the engine does not define, and no others.

The set is computed when routes are loaded, as the host’s helper names minus the engine’s, and each one is defined to call main_app. It is rebuilt whenever routes reload, so a name that goes away goes away here too.

# In a host's ApplicationController, running inside Janela:
redirect_to new_session_path   # works, forwarded to the host
frames_path                    # Janela's, never the host's /frames
main_app.frames_path           # the host's, said unambiguously

Two properties matter more than the convenience:

The engine’s own routes can never be shadowed. A name is forwarded only when Janela does not define it, so no host route can change what one of Janela’s own views means. Precedence is decided by construction, not by the order modules happened to be included.

main_app still means what it means. Nothing is taken away. A host that prefers to be explicit, or that needs a name Janela also uses, says main_app. and is unambiguous.

This covers named helpers only. url_for(record) and redirect_to @record resolve polymorphically at call time, so there is no name to forward and they still need main_app.. That boundary is documented rather than papered over.

Consequences

  • A host’s authentication redirect works inside Janela with no configuration, no reopened class and no knowledge that an engine is involved. That was the whole complaint in three reports.
  • Janela is slightly less isolated than a textbook engine, in one direction only: host names in, never Janela names out. ADR 005’s mount independence is untouched, since the engine still generates every one of its own URLs through its own routes.
  • A host route helper shares a name with one of Janela’s and the host loses the bare name inside the engine. Janela’s route names are few and dull, which makes this rare, and main_app. remains.
  • The README’s explanation was wrong about the cause and is corrected here and there.
  • Polymorphic routing is a known gap. If it bites someone the answer is another ADR, not a quiet widening of this one.

All documentation This page in the repository