ruby_everywhere 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +255 -0
  3. data/bridge/README.md +48 -0
  4. data/bridge/everywhere/bridge.js +388 -3
  5. data/bridge/everywhere/native.css +203 -0
  6. data/bridge/package.json +6 -3
  7. data/lib/everywhere/android_resources.rb +217 -0
  8. data/lib/everywhere/android_sdk.rb +330 -0
  9. data/lib/everywhere/asset_catalog.rb +185 -0
  10. data/lib/everywhere/auth_handoff.rb +398 -0
  11. data/lib/everywhere/auth_token.rb +118 -0
  12. data/lib/everywhere/builders/android.rb +844 -0
  13. data/lib/everywhere/builders/ios.rb +72 -3
  14. data/lib/everywhere/commands/build.rb +51 -9
  15. data/lib/everywhere/commands/clean.rb +10 -5
  16. data/lib/everywhere/commands/dev.rb +46 -8
  17. data/lib/everywhere/commands/doctor.rb +89 -3
  18. data/lib/everywhere/commands/install.rb +1 -1
  19. data/lib/everywhere/commands/logs.rb +117 -8
  20. data/lib/everywhere/config.rb +513 -14
  21. data/lib/everywhere/emulator.rb +405 -0
  22. data/lib/everywhere/engine.rb +21 -0
  23. data/lib/everywhere/icon.rb +196 -0
  24. data/lib/everywhere/log_filter.rb +74 -0
  25. data/lib/everywhere/mobile_config_endpoint.rb +13 -0
  26. data/lib/everywhere/mobile_configs_controller.rb +18 -0
  27. data/lib/everywhere/native_helper.rb +198 -5
  28. data/lib/everywhere/omniauth.rb +88 -0
  29. data/lib/everywhere/paths.rb +47 -0
  30. data/lib/everywhere/version.rb +1 -1
  31. data/support/mobile/android/README.md +217 -0
  32. data/support/mobile/android/app/build.gradle.kts +156 -0
  33. data/support/mobile/android/app/everywhere.properties +8 -0
  34. data/support/mobile/android/app/native-packages.gradle.kts +16 -0
  35. data/support/mobile/android/app/proguard-rules.pro +21 -0
  36. data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
  37. data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
  38. data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
  39. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
  40. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
  41. data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
  42. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
  43. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
  44. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
  45. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
  46. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
  47. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
  48. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
  49. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
  50. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
  51. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
  52. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
  53. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
  54. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
  55. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
  56. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
  57. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
  58. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
  59. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
  60. data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
  61. data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
  62. data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
  63. data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
  64. data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
  65. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
  66. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
  67. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
  68. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
  69. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
  70. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  71. data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
  72. data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
  73. data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
  74. data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
  75. data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
  76. data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
  77. data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
  78. data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
  79. data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
  80. data/support/mobile/android/build.gradle.kts +8 -0
  81. data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  82. data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  83. data/support/mobile/android/gradle.properties +14 -0
  84. data/support/mobile/android/gradlew +251 -0
  85. data/support/mobile/android/gradlew.bat +94 -0
  86. data/support/mobile/android/settings.gradle.kts +28 -0
  87. data/support/mobile/ios/App/AppDelegate.swift +7 -1
  88. data/support/mobile/ios/App/AuthFlow.swift +101 -0
  89. data/support/mobile/ios/App/Bridge/MenuComponent.swift +192 -0
  90. data/support/mobile/ios/App/EverywhereConfig.swift +105 -0
  91. data/support/mobile/ios/App/SceneDelegate.swift +144 -0
  92. data/support/mobile/ios/App.xcodeproj/project.pbxproj +8 -0
  93. data/support/mobile/ios/README.md +10 -0
  94. metadata +69 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bb96376a66f4202388542212bc1d03b2dea1bb6519ccc4a8339cc1b3cda3e43
4
- data.tar.gz: be5f8b69e0157a93dfc1837445faa8f4e6326678741b88ddd76c9bff0521ddf5
3
+ metadata.gz: 7ffb921df597efe047c25240e7d8b92aaad49c00c90d546020e053eac618739a
4
+ data.tar.gz: 17315b4c5d91eced13b44d7147942d7d6357ee791b8622425ab655eb97b1ab27
5
5
  SHA512:
6
- metadata.gz: 29c23a79521c57f81a0881e663debc4e2c9a505a424dbe8b615c1a1c5eccba3d93c1dfd7ee0281ea04c8121d2c4ba6ec4c0de8276453ab5ca0d348d6d8afdf00
7
- data.tar.gz: 7a626876bb0820701922e23990541e149360b35246b2456be3b2d5f7729743f2c5e3772d6e9e2d94d7ea898b85d6e528ff5e9ad85e919653357463073cfbdcea
6
+ metadata.gz: 71d9ba1ea63abe51a7262f02f069ef9f068d3192bb20800bdd0fba8bcd50f36f2daa83bb3f758e7273576b69b88be5541428b41f93021579cc1b5054532099ad
7
+ data.tar.gz: 59632bacae03ade28233832d0193d2500e4e3f85c07b7e61a39f1e086cc398b40f66d48091ffc9e336a11e2f91994b666ea60fc7ab2cbef2b76252d6fb0ada60
data/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # RubyEverywhere
2
+
3
+ **Ship desktop and mobile apps from the Ruby web apps you already know how to build.**
4
+
5
+ [rubyeverywhere.com](https://rubyeverywhere.com) · by [Andrea Fomera](https://afomera.dev)
6
+
7
+ RubyEverywhere takes a Rails (or Sinatra, or Hanami) app and gives it a native
8
+ life on the platforms your users are already on:
9
+
10
+ - **Desktop** — a [Tauri](https://tauri.app)-based shell. In *local* mode your
11
+ whole app is pressed into a single-file binary with
12
+ [Tebako](https://github.com/tamatebako/tebako) and runs on-device, no Ruby
13
+ install required; in *remote* mode the shell is a thin native wrapper around
14
+ your deployed app.
15
+ - **iOS** — a [Hotwire Native](https://native.hotwired.dev) shell with a real
16
+ tab bar, nav-bar buttons, menus, action sheets, notifications, haptics,
17
+ biometrics, deep links, and native sign-in.
18
+ - **Android** — a feature-for-feature Hotwire Native Android port of the same
19
+ shell, stamped from the same `config/everywhere.yml`.
20
+
21
+ One config file, one CLI, one JavaScript API. The web app stays the source of
22
+ truth — most of what you'd normally rebuild natively (tabs, routes, menus,
23
+ badges) deploys with your Rails app rather than through an app-store review.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ gem install ruby_everywhere
29
+ ```
30
+
31
+ This gives you two equivalent executables — `every` and `rbe` — plus the
32
+ packaging toolchain (the `rbe-tebako` fork) as a dependency.
33
+
34
+ Or add it to your app's Gemfile, which also wires up the Rails engine (bridge
35
+ asset serving, importmap pin, view helpers, the live mobile config endpoint):
36
+
37
+ ```ruby
38
+ gem "ruby_everywhere"
39
+ ```
40
+
41
+ Requires Ruby >= 3.2.
42
+
43
+ **The gem is free, and it always will be.** Everything here — the CLI, the
44
+ desktop shell, both mobile shells, the bridge — is MIT licensed and yours to
45
+ use, forever, with no account and no key. Local builds run on a Mac today
46
+ (pressing, code signing, Xcode and the Android SDK all want a macOS host).
47
+ If you don't own one, or you'd rather not keep a build toolchain healthy, the
48
+ [RubyEverywhere Platform](https://rubyeverywhere.com) is a paid service coming
49
+ soon that runs the exact same builds for you — see
50
+ [Hosted builds](#hosted-builds) below. It's a convenience, never a gate: nothing
51
+ in this gem is held back for it.
52
+
53
+ ## Quick start
54
+
55
+ ```sh
56
+ cd path/to/your/app
57
+
58
+ every install # remote mode: a native shell around your deployed app
59
+ every install --local # local mode: compile and ship the app itself
60
+
61
+ every doctor # check the toolchain before your first build
62
+
63
+ every dev # dev server + native shells on demand, live reload
64
+ every dev --ios # ...straight into the iOS Simulator
65
+ every dev --android # ...straight into the Android emulator
66
+
67
+ every build # package the macOS .app
68
+ every build --ios # build the iOS shell
69
+ every build --android # build the APK/AAB
70
+ ```
71
+
72
+ `every install` is idempotent — it adds `config/everywhere.yml`, a boot stub,
73
+ and the framework-specific config tweaks it needs, and skips anything already
74
+ in place. For Rails it does the surgery for you; for Sinatra and Hanami it
75
+ vendors the bridge and prints the remaining wiring rather than guessing at your
76
+ boot files.
77
+
78
+ `every dev` never packages anything: it runs your ordinary dev server and points
79
+ the native shells at it, so the edit-refresh loop is untouched Rails.
80
+
81
+ ## Configuration
82
+
83
+ Everything per-app lives in `config/everywhere.yml`. The native shell templates
84
+ are frozen — the CLI stamps a copy per app and only ever writes the identity,
85
+ config, icon, and color files, so a shell upgrade never collides with your app.
86
+
87
+ ```yaml
88
+ app:
89
+ name: My Really Awesome App
90
+ bundle_id: com.example.myapp # set it once; renaming moves users' data
91
+ version: 1.2.0
92
+ entry_path: /dashboard # where the app opens
93
+ icon: icon.png
94
+
95
+ appearance:
96
+ tint_color: "#CC342D"
97
+ background_color: # one hex string, or split by system theme
98
+ light: "#FAF9F7"
99
+ dark: "#1C1B1A"
100
+
101
+ remote:
102
+ url: https://app.example.com # remote mode: the deployed app to wrap
103
+
104
+ tabs: # the native mobile tab bar
105
+ - name: Builds
106
+ path: /builds
107
+ icons:
108
+ ios: hammer # SF Symbol
109
+ android: build # Material icon
110
+
111
+ permissions: # only declared permissions can ever prompt
112
+ notifications: true
113
+ camera: "Scan QR codes to pair devices." # the sentence iOS shows
114
+
115
+ rules: # Hotwire Native path configuration
116
+ - patterns: ["/preferences$"]
117
+ properties:
118
+ context: modal
119
+
120
+ build:
121
+ ruby: "4.0.6"
122
+ targets: [macos-arm64, ios-arm64, android-arm64]
123
+
124
+ updates: # self-hosted, signed auto-updates
125
+ url: https://updates.example.com
126
+ public_key: RWQ…
127
+ auto: check # off | check | download | install
128
+ ```
129
+
130
+ Also supported: `auth:` (native OAuth sign-in with a callback scheme),
131
+ `deep_linking:` (universal links / App Links — the gem serves the
132
+ `apple-app-site-association` and `assetlinks.json` files for you), and
133
+ `native:` for "supernative" extensions — Swift you keep in your app repo under
134
+ `native/ios/` that `every build` compiles into the shell, including SwiftUI
135
+ screens, bridge components, a custom splash, and pinned SPM packages.
136
+
137
+ Tabs and path rules ship two ways from that one source: baked into the app
138
+ bundle for offline/first launch, and served live from
139
+ `/everywhere/{ios,android}_v1.json`, so changing them deploys with your web app.
140
+ `Everywhere.filter_tabs` lets you vary the tab bar per request — it shares the
141
+ app's session, so signed-out users can get a different bar without a release.
142
+
143
+ ## The JavaScript bridge
144
+
145
+ The gem ships `@rubyeverywhere/bridge` — one API for the browser, the desktop
146
+ shell, and the mobile shells. Rails apps get it served and importmap-pinned
147
+ automatically; Sinatra and Hanami get it vendored to `public/bridge.js`.
148
+
149
+ ```js
150
+ import Everywhere from "@rubyeverywhere/bridge"
151
+
152
+ Everywhere.platform // "desktop" | "mobile" | "browser"
153
+ Everywhere.native // true inside a RubyEverywhere app
154
+
155
+ Everywhere.notify({ title, body })
156
+ Everywhere.menu({ title, items })
157
+ Everywhere.haptics.impact("medium")
158
+ Everywhere.badge.setTab("/inbox", 3)
159
+ Everywhere.permissions.request("camera")
160
+ Everywhere.storage.set("theme", "dark")
161
+ Everywhere.updates.check()
162
+ ```
163
+
164
+ Everything degrades gracefully — the same page works in a plain browser tab and
165
+ gains native powers inside the app. See
166
+ [`bridge/README.md`](bridge/README.md) for the full surface.
167
+
168
+ Most native chrome needs no JavaScript at all. Tag ordinary markup and the
169
+ bridge lifts it into real native controls; tapping the native control just
170
+ `.click()`s the element it mirrors, so behavior stays defined once in the DOM.
171
+ Rails ships helpers that emit exactly that markup:
172
+
173
+ ```erb
174
+ <%= everywhere_nav_button "New", new_note_path, icons: { ios: "plus", android: "add" } %>
175
+ <%= everywhere_fab new_note_path %>
176
+ <%= everywhere_nav_menu "More" do %>
177
+ <%= everywhere_menu_item "Delete", note_path(@note), method: :delete, style: "destructive" %>
178
+ <% end %>
179
+ ```
180
+
181
+ Plus `native_app?`, `native_platform`, `everywhere_badge`,
182
+ `everywhere_biometric_lock`, and friends.
183
+
184
+ ## Commands
185
+
186
+ | Command | What it does |
187
+ | --- | --- |
188
+ | `every install` | Prepare an app (boot stub, `config/everywhere.yml`, config tweaks) |
189
+ | `every doctor` | Check the toolchain — Tebako, Rust, brew deps, Xcode, Android SDK/JDK |
190
+ | `every dev` | Dev server with native shells on demand (`--desktop --ios --android --browser`) |
191
+ | `every logs` | Stream shell logs from the Simulator or Android device |
192
+ | `every build` | Package natively — desktop, `--ios`, or `--android` |
193
+ | `every icon` | Generate `.icns` / `.ico` / Linux icons from a source PNG |
194
+ | `every release` | Sign, notarize, staple, and emit a `release.json` receipt |
195
+ | `every updates keygen` | Generate a minisign-compatible update-signing keypair |
196
+ | `every publish` | Publish a release to your update bucket |
197
+ | `every clean` | Remove the shell build caches |
198
+ | `every shell-dir` | Print the path of the bundled shell template |
199
+ | `every platform …` | Hosted builds: `login`, `logout`, `auth status`, `build` |
200
+
201
+ Run `every` with no arguments for the full list, or `every <command> --help` for
202
+ a command's flags.
203
+
204
+ ## Auto-updates
205
+
206
+ Desktop releases can update themselves from a bucket you own, with no service in
207
+ the middle:
208
+
209
+ ```sh
210
+ every updates keygen # once, per app
211
+ every release # build, sign, notarize, staple
212
+ every publish --notes-file NOTES.md # sign the manifest, upload, flip latest.json
213
+ ```
214
+
215
+ The public key ships in the app; the secret key never leaves your machine. Apps
216
+ can check, install, and switch channels at runtime via `Everywhere.updates`.
217
+
218
+ ## Hosted builds
219
+
220
+ Don't have a Mac, or want iOS and Android built without babysitting Xcode, the
221
+ Android SDK, and a signing setup? The **RubyEverywhere Platform** — a paid
222
+ service, coming soon — runs the same builds on managed machines and hands back
223
+ the artifacts. The CLI already speaks to it:
224
+
225
+ ```sh
226
+ every platform login
227
+ every platform build --target ios-arm64,android-arm64
228
+ ```
229
+
230
+ Same gem, same `config/everywhere.yml`, same output — you're paying for the
231
+ build machines and the toolchain upkeep, not for features. The gem stays free
232
+ and complete on its own.
233
+
234
+ ## Project layout
235
+
236
+ ```
237
+ lib/everywhere/ the CLI, config, builders, and Rails engine
238
+ exe/every, exe/rbe the executables
239
+ bridge/ @rubyeverywhere/bridge — the JS half
240
+ support/shell/ the Tauri desktop shell template
241
+ support/mobile/ios/ the Hotwire Native iOS shell template
242
+ support/mobile/android/ the Hotwire Native Android shell template
243
+ ```
244
+
245
+ The shell templates have their own READMEs documenting the freeze rules — what
246
+ the CLI stamps per app and what it must never touch:
247
+ [iOS](support/mobile/ios/README.md) · [Android](support/mobile/android/README.md).
248
+
249
+ ## Contributing
250
+
251
+ Bug reports and pull requests are welcome. Run the test suite with `rake test`.
252
+
253
+ ## License
254
+
255
+ Released under the [MIT License](LICENSE.txt). Copyright © 2026 Andrea Fomera.
data/bridge/README.md CHANGED
@@ -33,6 +33,11 @@ Everywhere.confirm("Sure?") // Promise<boolean>, native dialog when pos
33
33
  Everywhere.on("menu", handler) // shell events; returns an unsubscribe fn
34
34
  Everywhere.visit("/settings") // Turbo.visit with location fallback
35
35
 
36
+ Everywhere.menu({ title, items }) // Promise<item | null> — native action
37
+ // sheet in the shell, a styled bottom
38
+ // sheet everywhere else. items:
39
+ // [{ title, id?, style?, disabled? }]
40
+
36
41
  Everywhere.clipboard.write(text) // Promise<void>
37
42
  Everywhere.clipboard.read() // Promise<string | null>
38
43
 
@@ -61,6 +66,49 @@ Everywhere.storage.clear() // Promise<boolean> — clears app keys onl
61
66
  // desktop shell. Settings, not secrets — tokens go in biometrics.credential.
62
67
  ```
63
68
 
69
+ ### Native chrome (nav bar buttons, menus, action sheets)
70
+
71
+ Tag ordinary markup, and inside the mobile shell the bridge lifts it into real
72
+ native controls — a nav-bar button, a pull-down / overflow menu, or an action
73
+ sheet. Tapping the native control just `.click()`s the element it mirrors, so a
74
+ link navigates, a submit submits, and behavior stays defined once in the DOM. In
75
+ a browser the same markup is the plain HTML it already is. No JavaScript, no
76
+ Stimulus — CSP-safe.
77
+
78
+ ```html
79
+ <!-- becomes a nav-bar button (right side); icons are per-platform like tabs -->
80
+ <a href="/notes/new" data-everywhere-nav-button
81
+ data-everywhere-nav-title="New"
82
+ data-everywhere-nav-icon-ios="plus"
83
+ data-everywhere-nav-icon-android="add">New</a>
84
+
85
+ <!-- a submit button in the nav bar -->
86
+ <button type="submit" data-everywhere-nav-button
87
+ data-everywhere-nav-title="Save" data-everywhere-nav-style="done">Save</button>
88
+
89
+ <!-- a nav-bar pull-down / overflow menu (defaults to the ⋯ icon) -->
90
+ <div data-everywhere-nav-menu data-everywhere-nav-title="More">
91
+ <a href="/share" data-everywhere-menu-item data-everywhere-menu-title="Share">Share</a>
92
+ <button form="delete_1" data-everywhere-menu-item
93
+ data-everywhere-menu-title="Delete"
94
+ data-everywhere-menu-style="destructive">Delete</button>
95
+ </div>
96
+
97
+ <!-- an in-content action sheet: a trigger and the items it opens -->
98
+ <div data-everywhere-menu>
99
+ <button data-everywhere-menu-trigger>Options</button>
100
+ <div data-everywhere-menu-items>
101
+ <a href="/x/edit" data-everywhere-menu-item>Edit</a>
102
+ </div>
103
+ </div>
104
+ ```
105
+
106
+ Rails ships helpers that emit exactly this — `everywhere_nav_button`,
107
+ `everywhere_submit_button`, `everywhere_nav_menu` / `everywhere_menu_item`,
108
+ `everywhere_menu`, and `everywhere_fab` (a floating action button). Include
109
+ `everywhere/native.css` for the FAB, the action-sheet fallback, and the
110
+ safe-area utilities.
111
+
64
112
  ### Multi-instance apps
65
113
 
66
114
  Apps whose users pick a server — one hosted platform, many instances — opt in