Binary Solo

Directing Turbo Native apps from the server

posted by Ayush Newatia on December 1st, 2022
This post was extracted and adapted from The Rails and Hotwire Codex. It also assumes some familiarity with Turbo Native.

When developing for the web, we can send the user to any location during a web request using an HTTP redirect (3xx status codes). Turbo Native effectively wraps the website within native navigation. This means a redirect may not always do the trick.

Let's take an example. A Login screen is presented modally in the native apps. After a successful login, we want to redirect the user to the home page. In the app, we'd want to dismiss the modal to reveal the screen under it allowing the user to continue with whatever they were doing. A conventional redirect won't work, we need to tell the app to just dismiss the modal.

Server driven native navigation

The turbo-rails gem draws three routes which instruct the native app to do something. These routes don't return any meaningful content; the app is meant to intercept visit proposals to these routes and implement logic to execute the instruction. The routes, and what they instruct, are:

  • /recede_historical_location -> The app should go back
  • /resume_historical_location -> The app should do nothing in terms of navigation
  • /refresh_historical_location -> The app should refresh the current page

Check out the source code to see the routes and the actions they point to.

These routes are exclusively for the native apps and have no meaning on web. To help with this, turbo-rails has methods to conditionally redirect a user if they're using the app. For example, instead of redirecting as below:

redirect_to root_path, status: :see_other

We would use:

recede_or_redirect_to root_path, status: :see_other

This will redirect to / on the web, and to /recede_historical_location in the native apps. Check out all the available redirect methods in the source code.

Using these methods, the app can be directed to dismiss the login modal after logging in while still redirecting to the root path on web.

Next, redirects to the aforementioned paths need to be intercepted and handled in the apps. We'll call these paths: path directives, since they direct the app to do something.

Intercept and handle path directives

Let's look at iOS. The following delegate method is triggered for every web request from the app.

extension RoutingController: SessionDelegate {
  func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
    // ...

In here, we can check if a visit is a path directive and act accordingly.

extension RoutingController: SessionDelegate {
  func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
    if proposal.isPathDirective {
    } else {

  func executePathDirective(_ proposal: VisitProposal) {
    guard proposal.isPathDirective else { return }

    switch proposal.url.path {
    case "/recede_historical_location":
    case "/refresh_historical_location":

  private func dismissOrPopViewController() {
    // ...

  private func refreshWebView() {
   // ...

extension VisitProposal {
  var isPathDirective: Bool {
    return url.path.contains("_historical_location")

That'll do the trick on iOS. 

Building a Turbo Native navigation system is non-trivial, so I've had to omit a lot of surrounding code to keep this post on point. My book fills in all the gaps if you're interested in digging deeper.

Next, let's look at Android. Custom navigation is handled by creating an interface which inherits from TurboNavDestination. In this interface, the shouldNavigateTo(newLocation: String) method is called for every web request. We can handle path directives in here.

interface NavDestination: TurboNavDestination {

  override fun shouldNavigateTo(newLocation: String): Boolean {
    return when {
      isPathDirective(newLocation) -> {
      else -> true

  private fun executePathDirective(url: String) {
    val url = URL(url)
    when (url.path) {
      "/recede_historical_location" -> navigateUp()
      "/refresh_historical_location" -> refresh()

  private fun isPathDirective(url: String): Boolean {
    return url.contains("_historical_location")

  // ...


That's how Turbo Native powered apps can be controller from the server! This gives us an immense amount of flexibility and extensibility without ever deploying app updates.

If you liked this post, check out my book, The Rails and Hotwire Codex, to level-up your Rails and Hotwire skills! It'll teach you how to build a Turbo Native navigation system and fills in the gaps in this blog post.

Subscribe to Binary Solo

We'll send you an email every time a new post is published. We'll never spam you or sell your email address to third parties. Check out our privacy policy for details.