RubyEverywhere

Ship desktop and mobile apps from the Ruby web apps you already know how to build.

rubyeverywhere.com · by Andrea Fomera

RubyEverywhere takes a Rails (or Sinatra, or Hanami) app and gives it a native life on the platforms your users are already on:

  • Desktop — a Tauri-based shell. In local mode your whole app is pressed into a single-file binary with Tebako and runs on-device, no Ruby install required; in remote mode the shell is a thin native wrapper around your deployed app.
  • iOS — a Hotwire Native shell with a real tab bar, nav-bar buttons, menus, action sheets, notifications, haptics, biometrics, deep links, and native sign-in.
  • Android — a feature-for-feature Hotwire Native Android port of the same shell, stamped from the same config/everywhere.yml.

One config file, one CLI, one JavaScript API. The web app stays the source of truth — most of what you'd normally rebuild natively (tabs, routes, menus, badges) deploys with your Rails app rather than through an app-store review.

Install

gem install ruby_everywhere

This gives you two equivalent executables — every and rbe — plus the packaging toolchain (the rbe-tebako fork) as a dependency.

Or add it to your app's Gemfile, which also wires up the Rails engine (bridge asset serving, importmap pin, view helpers, the live mobile config endpoint):

gem "ruby_everywhere"

Requires Ruby >= 3.2.

The gem is free, and it always will be. Everything here — the CLI, the desktop shell, both mobile shells, the bridge — is MIT licensed and yours to use, forever, with no account and no key. Local builds run on a Mac today (pressing, code signing, Xcode and the Android SDK all want a macOS host). If you don't own one, or you'd rather not keep a build toolchain healthy, the RubyEverywhere Platform is a paid service coming soon that runs the exact same builds for you — see Hosted builds below. It's a convenience, never a gate: nothing in this gem is held back for it.

Quick start

cd path/to/your/app

every install          # remote mode: a native shell around your deployed app
every install --local  # local mode: compile and ship the app itself

every doctor           # check the toolchain before your first build

every dev              # dev server + native shells on demand, live reload
every dev --ios        # ...straight into the iOS Simulator
every dev --android    # ...straight into the Android emulator

every build            # package the macOS .app
every build --ios      # build the iOS shell
every build --android  # build the APK/AAB

every install is idempotent — it adds config/everywhere.yml, a boot stub, and the framework-specific config tweaks it needs, and skips anything already in place. For Rails it does the surgery for you; for Sinatra and Hanami it vendors the bridge and prints the remaining wiring rather than guessing at your boot files.

every dev never packages anything: it runs your ordinary dev server and points the native shells at it, so the edit-refresh loop is untouched Rails.

Configuration

Everything per-app lives in config/everywhere.yml. The native shell templates are frozen — the CLI stamps a copy per app and only ever writes the identity, config, icon, and color files, so a shell upgrade never collides with your app.

app:
  name: My Really Awesome App
  bundle_id: com.example.myapp     # set it once; renaming moves users' data
  version: 1.2.0
  entry_path: /dashboard           # where the app opens
  icon: icon.png

appearance:
  tint_color: "#CC342D"
  background_color:                # one hex string, or split by system theme
    light: "#FAF9F7"
    dark: "#1C1B1A"

remote:
  url: https://app.example.com     # remote mode: the deployed app to wrap

tabs:                              # the native mobile tab bar
  - name: Builds
    path: /builds
    icons:
      ios: hammer                  # SF Symbol
      android: build               # Material icon

permissions:                       # only declared permissions can ever prompt
  notifications: true
  camera: "Scan QR codes to pair devices."   # the sentence iOS shows

rules:                             # Hotwire Native path configuration
  - patterns: ["/preferences$"]
    properties:
      context: modal

build:
  ruby: "4.0.6"
  targets: [macos-arm64, ios-arm64, android-arm64]

updates:                           # self-hosted, signed auto-updates
  url: https://updates.example.com
  public_key: RWQ…
  auto: check                      # off | check | download | install

Also supported: auth: (native OAuth sign-in with a callback scheme), deep_linking: (universal links / App Links — the gem serves the apple-app-site-association and assetlinks.json files for you), and native: for "supernative" extensions — Swift you keep in your app repo under native/ios/ that every build compiles into the shell, including SwiftUI screens, bridge components, a custom splash, and pinned SPM packages.

Tabs and path rules ship two ways from that one source: baked into the app bundle for offline/first launch, and served live from /everywhere/{ios,android}_v1.json, so changing them deploys with your web app. Everywhere.filter_tabs lets you vary the tab bar per request — it shares the app's session, so signed-out users can get a different bar without a release.

The JavaScript bridge

The gem ships @rubyeverywhere/bridge — one API for the browser, the desktop shell, and the mobile shells. Rails apps get it served and importmap-pinned automatically; Sinatra and Hanami get it vendored to public/bridge.js.

import Everywhere from "@rubyeverywhere/bridge"

Everywhere.platform          // "desktop" | "mobile" | "browser"
Everywhere.native            // true inside a RubyEverywhere app

Everywhere.notify({ title, body })
Everywhere.menu({ title, items })
Everywhere.haptics.impact("medium")
Everywhere.badge.setTab("/inbox", 3)
Everywhere.permissions.request("camera")
Everywhere.storage.set("theme", "dark")
Everywhere.updates.check()

Everything degrades gracefully — the same page works in a plain browser tab and gains native powers inside the app. See bridge/README.md for the full surface.

Most native chrome needs no JavaScript at all. Tag ordinary markup and the bridge lifts it into real native controls; tapping the native control just .click()s the element it mirrors, so behavior stays defined once in the DOM. Rails ships helpers that emit exactly that markup:

<%= everywhere_nav_button "New", new_note_path, icons: { ios: "plus", android: "add" } %>
<%= everywhere_fab new_note_path %>
<%= everywhere_nav_menu "More" do %>
  <%= everywhere_menu_item "Delete", note_path(@note), method: :delete, style: "destructive" %>
<% end %>

Plus native_app?, native_platform, everywhere_badge, everywhere_biometric_lock, and friends.

Commands

Command What it does
every install Prepare an app (boot stub, config/everywhere.yml, config tweaks)
every doctor Check the toolchain — Tebako, Rust, brew deps, Xcode, Android SDK/JDK
every dev Dev server with native shells on demand (--desktop --ios --android --browser)
every logs Stream shell logs from the Simulator or Android device
every build Package natively — desktop, --ios, or --android
every icon Generate .icns / .ico / Linux icons from a source PNG
every release Sign, notarize, staple, and emit a release.json receipt
every updates keygen Generate a minisign-compatible update-signing keypair
every publish Publish a release to your update bucket
every clean Remove the shell build caches
every shell-dir Print the path of the bundled shell template
every platform … Hosted builds: login, logout, auth status, build

Run every with no arguments for the full list, or every <command> --help for a command's flags.

Auto-updates

Desktop releases can update themselves from a bucket you own, with no service in the middle:

every updates keygen                  # once, per app
every release                         # build, sign, notarize, staple
every publish --notes-file NOTES.md   # sign the manifest, upload, flip latest.json

The public key ships in the app; the secret key never leaves your machine. Apps can check, install, and switch channels at runtime via Everywhere.updates.

Hosted builds

Don't have a Mac, or want iOS and Android built without babysitting Xcode, the Android SDK, and a signing setup? The RubyEverywhere Platform — a paid service, coming soon — runs the same builds on managed machines and hands back the artifacts. The CLI already speaks to it:

every platform login
every platform build --target ios-arm64,android-arm64

Same gem, same config/everywhere.yml, same output — you're paying for the build machines and the toolchain upkeep, not for features. The gem stays free and complete on its own.

Project layout

lib/everywhere/          the CLI, config, builders, and Rails engine
exe/every, exe/rbe       the executables
bridge/                  @rubyeverywhere/bridge — the JS half
support/shell/           the Tauri desktop shell template
support/mobile/ios/      the Hotwire Native iOS shell template
support/mobile/android/  the Hotwire Native Android shell template

The shell templates have their own READMEs documenting the freeze rules — what the CLI stamps per app and what it must never touch: iOS · Android.

Contributing

Bug reports and pull requests are welcome. Run the test suite with rake test.

License

Released under the MIT License. Copyright © 2026 Andrea Fomera.