The project is in a healthy, maintained state
Host-neutral XML rendering, Frames, Streams, validation, and Action Cable integration for Expo Turbo
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.2, < 8.2
>= 7.2, < 8.2
>= 1.15.7, < 2
>= 7.2, < 8.2
>= 2.0.10, < 3
 Project Readme

Expo Turbo

npm version RubyGems version CI

Important

This project is 100% AI-generated. It began as a goal given to Codex and took six days of iterative implementation, reproduction, testing, and correction—not a one-shot generation. We did not meter the exact cost, but estimate that the work used at least a few tens of thousands of dollars' worth of inference. The goal is to bring Turbo's server-driven navigation, Frames, Streams, forms, and Action Cable model to Expo React Native while keeping the UI native.

Contributions are welcome, with one caveat: please open a guided bug report or change request with context, not a code patch. Pull requests are disabled because the maintainers did not write this code and cannot responsibly judge its implementation-level best practices. Write for an agent starting with zero context: separate facts from hypotheses; include the problem and impact, exact reproduction and evidence, expected behavior and constraints, plus how the fix should be validated. You may investigate locally with your own agent and paste its compressed findings and proposed solution. Our coding agents will reproduce the problem, implement the change, and validate the fix.

Expo Turbo lets a Rails application send Turbo-style pages and updates to an Expo React Native application.

Rails renders XML instead of browser HTML. The Expo app reads that XML and maps its element names to React Native components that the app has already registered. This gives Rails and Expo a shared format for documents, links, forms, Frames, Streams, and Action Cable updates while the mobile app keeps control of its native UI and behavior.

Why it exists

Turbo already gives Rails applications a way to update browser pages without building a separate client-side API for every interaction. Expo Turbo brings the supported parts of that model to React Native, where there is no browser DOM.

It is not a browser or a way for the server to run arbitrary code in an app. Rails may select only components and values that the Expo app explicitly allows. The app still owns navigation, authentication, networking, native styles, accessibility, and product state.

How Rails and Expo work together

  1. A Rails controller uses the expo_turbo-rails gem to render a validated XML document, Frame, or Stream response.
  2. The expo-turbo TypeScript package parses that response and applies the supported Turbo behavior.
  3. The Expo app renders registered React Native components and supplies the platform-specific behavior the runtime needs.
  4. Rails continues to own routes, authorization, views, caching, and broadcasts.

In the phrase host-neutral XML protocol runtime:

  • XML protocol means Rails and Expo exchange a defined set of XML documents and updates.
  • Runtime means the package reads those messages and carries out supported visits, form submissions, Frame loads, and Stream updates.
  • Host-neutral means the core does not assume a particular app, router, network client, storage system, or authentication scheme. The Expo app provides those pieces through adapters.

The implementation targets the supported native equivalents of Turbo 8.0.23 semantics. It does not try to copy browser-only DOM behavior.

Important

0.1.0 is the stable release published on 2026-07-27. The package, gem, standalone examples, automated conformance suite, and installed iOS and Android simulator Release audits are complete for the supported surface. The physical Android and iOS union suites also pass all 71 contracts and 115 atomic observations on each platform, including native provider multipart proof. Manual VoiceOver, TalkBack, and browser screen-reader evidence remains an explicit follow-up and is not claimed by the 0.1.0 compatibility surface.

Try it from source

To inspect and test the current source:

git clone https://github.com/noscrubs-dev/expo-turbo.git
cd expo-turbo
bun install --frozen-lockfile
bun run check

Start with the getting-started guide. The standalone Expo app and standalone Rails host are the canonical integration examples.

Release status

Area Current status
TypeScript package expo-turbo@0.1.0; stable and clean-imported through all six public entrypoints
Rails gem expo_turbo-rails 0.1.0; clean-installed against turbo-rails 2.0.10 and 2.0.23
Protocol baseline Turbo 8.0.23; all 26 upstream functional-suite families classified
Native Release evidence Installed iOS Simulator and Android Emulator builds pass; physical Android and iOS each pass 71/71 shared contracts
Stable publication v0.1.0, published 2026-07-27 from one verified frozen npm/gem candidate

See the machine-readable compatibility manifest and 0.1.0 release-readiness checklist for the exact boundary.

Install

bun add expo-turbo
bundle add expo_turbo-rails

Detailed support checklist

A checked item is implemented and covered by the current public source and test suite. This is the detailed technical reference; newcomers can start with the overview and examples above. “Native equivalent” means the protocol outcome is supported through explicit native adapters; it does not claim browser DOM behavior.

Protocol and runtime

  • Strict, namespace-aware UTF-8 XML documents
  • Ordered multi-root Turbo Stream fragments
  • Byte, depth, node, attribute, text, and Stream-action limits
  • DTD, entity-declaration, and processing-instruction rejection
  • Preserved mixed content, CDATA, entities, and xml:space
  • Addressable document tree with stable keys and unique-ID enforcement
  • Case-sensitive XML selectors for supported structural target queries
  • Deterministic diagnostics and redacted typed protocol errors

React Native host

  • Zod-backed typed component, component-action, and custom Stream registries
  • Explicit attribute codecs, child slots, aliases, and capability hashes
  • ExpoTurboProvider, ExpoTurboRoot, boundaries, hooks, and scoped state
  • Small-slice external-store subscriptions and deterministic disposal
  • Semantic style-token and direction adapters
  • Host-owned focus, scroll, visibility, storage, navigation, and observability adapters
  • Accessible busy, pending, disabled, validation, retry, and announcement surfaces

Visits, links, history, and Frames

  • Same-origin document and Frame GETs with Turbo request headers
  • Redirect, empty, authoritative error, wrong-MIME, malformed, abort, and supersession outcomes
  • advance, replace, and native restore visits
  • Ten-entry snapshot cache, previews, preloads, and touch-native press-in prefetch
  • Host-acknowledged history identity plus root-scroll restoration
  • Same-document and host-routed cross-document native anchors
  • Eager, lazy, nested, recurse, disabled, targeted, and programmatic Frames
  • Matching-Frame extraction, missing-Frame handling, redirects, and request epochs
  • Native Frame loading, busy/complete, autofocus, autoscroll, and refresh-morph equivalents

Forms

  • Host-registered successful controls and submitter precedence
  • GET, URL-encoded, text/plain, and multipart request planning
  • Rails method normalization and nested/repeated parameter names
  • Constraint validation, async confirmation, duplicate prevention, and submits-with
  • Matching Frame, direct Stream, 303, 422, 4xx/5xx, 201, and 204 handling
  • Component-owned draft preservation through compatible morphs
  • Bounded host-provided Blob/file entries and native picker adapters
  • Installed Android Emulator multipart proof through the real Rails host
  • Physical Android fallback-Blob and Files-picker multipart proof
  • Physical iOS fallback-Blob and Files-picker multipart proof

Streams and morphing

  • append, prepend, replace, update, remove, before, and after
  • Exact target, structural targets, template cloning, and collision semantics
  • Per-action ordering, isolated failures, custom actions, and lifecycle hooks
  • Refresh debounce, request-ID suppression, replace/morph, and scroll policy
  • Compatible stable-ID and anonymous native reconciliation
  • Permanent application, Frame, and Cable-source preservation
  • Component preserve/reset policy and bounded focus restoration
  • Document, Frame, Stream, element, and attribute morph lifecycle equivalents

Action Cable

  • Strict actioncable-v1-json wire codec and same-origin endpoint admission
  • One-socket WebSocket adapter with exact identifier refcounting
  • Welcome, confirmation, rejection, ping, disconnect, retry, and heartbeat handling
  • Lifecycle/reachability suspension, credential rotation, and bounded retry policy
  • Document- and Frame-aware reconciliation after reconnect
  • Public XML Stream namespaces and protected host-authorized subscriptions
  • Short-lived header-ticket, resource-grant, revocation, and rotation example
  • Physical Android lifecycle, network, protected-Cable, and renderer-flush evidence
  • Physical iOS lifecycle, network, protected-Cable, and renderer-flush evidence

Rails gem

  • Route-free Engine and opt-in ExpoTurbo::Rails::Controller
  • Distinct application/vnd.expo-turbo+xml document/Frame MIME type
  • Confined host-owned XML view roots and structural template validation
  • Component/style capability admission and duplicate-ID rejection
  • Frame, dom_id, cache-variation, and structural test helpers
  • All built-in Stream tags, sibling responses, immediate/queued broadcasts, and refresh debounce
  • Public and protected Action Cable source/channel/broadcast APIs
  • Rails/Action Cable 7.2–8.1 and turbo-rails 2.0.10–2.x dependency bounds
  • Real matrix coverage for turbo-rails 2.0.10 and 2.0.23

Conformance and release evidence

  • Shared TypeScript/Ruby protocol fixtures
  • All 26 pinned Turbo 8.0.23 functional-suite families classified
  • Executable official-Turbo differentials for the meaningful shared surface
  • Package, gem, Expo, Rails, boundary, security, and production-export gates
  • Installed iOS Simulator Release audit through real Rails
  • Installed signed Android Emulator Release audit through real Rails
  • Automated web WCAG 2.0/2.1 A/AA axe audit and accessibility-tree checks
  • Paired candidate build, clean installs, checksums, and provenance proof
  • Physical Android conformance evidence: 71/71 contracts and 115/115 observations
  • Physical iOS conformance evidence: 71/71 contracts and 115/115 observations
  • Manual VoiceOver, TalkBack, and browser screen-reader evidence
  • Final paired candidate from the final gated commit
  • Stable npm/RubyGems publication and clean registry verification

Responsibilities and limits

Expo Turbo reuses Turbo's message format and server behavior where they make sense on native platforms. It is not a browser DOM port of @hotwired/turbo.

  • Browser DOM targets, bubbling/composed paths, <head>/script/CSS behavior, selection ranges, shadow DOM, hover, and physical repaint timing are N/A.
  • Components, component actions, native styles, navigation/history mapping, focus/scroll/visibility handles, credentials, identity, origin policy, retry values, and product state belong to the Expo host.
  • Routes, controllers, views, authorization, cache inputs, tenant policy, grants, and product broadcasts belong to the Rails host.
  • Action Cable provides online delivery; missed-message correctness uses canonical reconciliation rather than durable replay.
  • Offline mutation replay is outside the Turbo contract.
  • Server XML can select only installed names and validated values. It cannot import or execute code.
  • Expo Turbo has no JSON fallback and does not require migration of an existing renderer.

Public entrypoints

Import Purpose
expo-turbo Version/status constants and combined public surface
expo-turbo/core Parser, tree/session, visits, Frames, forms, Streams, lifecycle, and errors
expo-turbo/adapters Host-neutral adapter interfaces and provided transport helpers
expo-turbo/react Provider, renderer, boundaries, and React hooks
expo-turbo/registry Typed component/action registries and attribute codecs
expo-turbo/testing Reserved testing boundary; no runtime APIs in 0.1.0
expo_turbo/rails Rails Engine, controller concern, helpers, broadcasts, and Cable integration
expo_turbo/rails/testing Opt-in strict structural XML test helpers

Deep source imports are unsupported.

Compatibility

Dependency Supported or validated range
Node.js >= 20.12
Bun source toolchain >= 1.3.14
React peer >= 19.1
Primary Expo example Expo SDK 57, React Native 0.86, Hermes
Ruby >= 3.2
Rails / Action Cable >= 7.2, < 8.2
turbo-rails >= 2.0.10, < 3; matrix pins 2.0.10 and 2.0.23
Protocol baseline Turbo 8.0.23

Repository layout

  • src/ — public TypeScript package source.
  • rails/ — public expo_turbo-rails gem and non-route-owning Rails Engine.
  • example/expo/ — standalone native consumer and compatibility gallery.
  • example/rails/ — standalone Rails and Action Cable host.
  • protocol/ — compatibility manifest, contract, and shared fixtures.
  • docs/ — getting started, decisions, release readiness, and evidence.
  • .maestro/ — checked-in native interaction flows.

The repository root is the publishable TypeScript package. It is intentionally not a package-manager workspace; both examples keep independent dependency state.

Development

Run the TypeScript package:

bun install --frozen-lockfile
bun run check

Run and build the Ruby gem:

cd rails
bundle install
bundle exec ruby "$(bundle show rake)/exe/rake"
bundle exec ruby "$(bundle show rake)/exe/rake" build

Install each example independently, then run both checks from the repository root:

cd example/expo && bun install --frozen-lockfile
cd ../rails && bundle install
cd ../..
bun run examples:check

Documentation

The manual Release workflow defaults to a non-publishing paired candidate. Its reviewer-gated publish mode may consume only a successful candidate for the exact final main commit and must reuse the downloaded npm and gem bytes without rebuilding them.

See CONTRIBUTING.md before proposing changes and SECURITY.md for private vulnerability reporting.

License

MIT. See LICENSE and NOTICE.md.