sessions 0.1.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +61 -0
  3. data/.simplecov +54 -0
  4. data/AGENTS.md +5 -0
  5. data/Appraisals +26 -0
  6. data/CHANGELOG.md +26 -0
  7. data/CLAUDE.md +5 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +454 -0
  10. data/Rakefile +32 -0
  11. data/app/assets/stylesheets/sessions.css +50 -0
  12. data/app/controllers/sessions/application_controller.rb +159 -0
  13. data/app/controllers/sessions/devices_controller.rb +48 -0
  14. data/app/helpers/sessions/engine_helper.rb +126 -0
  15. data/app/views/sessions/_device.html.erb +40 -0
  16. data/app/views/sessions/_devices.html.erb +34 -0
  17. data/app/views/sessions/_event.html.erb +13 -0
  18. data/app/views/sessions/_history.html.erb +20 -0
  19. data/app/views/sessions/devices/history.html.erb +5 -0
  20. data/app/views/sessions/devices/index.html.erb +15 -0
  21. data/config/locales/en.yml +59 -0
  22. data/config/locales/es.yml +59 -0
  23. data/config/routes.rb +17 -0
  24. data/docs/PRD.md +743 -0
  25. data/docs/research/01-carhey.md +250 -0
  26. data/docs/research/02-ecosystem.md +261 -0
  27. data/docs/research/03-rails-core.md +220 -0
  28. data/docs/research/04-devise-warden.md +249 -0
  29. data/docs/research/05-oauth.md +193 -0
  30. data/docs/research/06-prior-art.md +312 -0
  31. data/docs/research/07-device-detection.md +250 -0
  32. data/docs/research/08-rails8-landscape.md +216 -0
  33. data/docs/research/09-market-security.md +450 -0
  34. data/gemfiles/rails_7.1.gemfile +34 -0
  35. data/gemfiles/rails_7.2.gemfile +34 -0
  36. data/gemfiles/rails_8.0.gemfile +34 -0
  37. data/gemfiles/rails_8.1.gemfile +34 -0
  38. data/lib/generators/sessions/install_generator.rb +230 -0
  39. data/lib/generators/sessions/madmin_generator.rb +95 -0
  40. data/lib/generators/sessions/templates/add_sessions_columns.rb.erb +81 -0
  41. data/lib/generators/sessions/templates/create_sessions.rb.erb +101 -0
  42. data/lib/generators/sessions/templates/create_sessions_events.rb.erb +108 -0
  43. data/lib/generators/sessions/templates/initializer.rb +201 -0
  44. data/lib/generators/sessions/templates/madmin/event_resource.rb +77 -0
  45. data/lib/generators/sessions/templates/madmin/session_events_controller.rb +21 -0
  46. data/lib/generators/sessions/templates/madmin/session_resource.rb +65 -0
  47. data/lib/generators/sessions/templates/madmin/sessions_controller.rb +30 -0
  48. data/lib/generators/sessions/templates/session.rb.erb +14 -0
  49. data/lib/generators/sessions/templates/sessions_sweep_job.rb +20 -0
  50. data/lib/generators/sessions/views_generator.rb +33 -0
  51. data/lib/sessions/adapters/omakase.rb +195 -0
  52. data/lib/sessions/adapters/omniauth.rb +64 -0
  53. data/lib/sessions/adapters/warden.rb +293 -0
  54. data/lib/sessions/classifier.rb +208 -0
  55. data/lib/sessions/configuration.rb +441 -0
  56. data/lib/sessions/current.rb +20 -0
  57. data/lib/sessions/device.rb +411 -0
  58. data/lib/sessions/engine.rb +120 -0
  59. data/lib/sessions/errors.rb +24 -0
  60. data/lib/sessions/geolocation.rb +111 -0
  61. data/lib/sessions/ip_address.rb +56 -0
  62. data/lib/sessions/jobs/geolocate_job.rb +58 -0
  63. data/lib/sessions/macros.rb +26 -0
  64. data/lib/sessions/middleware.rb +41 -0
  65. data/lib/sessions/models/concerns/device_display.rb +134 -0
  66. data/lib/sessions/models/concerns/has_sessions.rb +116 -0
  67. data/lib/sessions/models/concerns/model.rb +513 -0
  68. data/lib/sessions/models/event.rb +293 -0
  69. data/lib/sessions/version.rb +5 -0
  70. data/lib/sessions.rb +423 -0
  71. metadata +225 -0
@@ -0,0 +1,250 @@
1
+ # Device intelligence: UA parsing, Hotwire Native, client hints, IP capture
2
+
3
+ Researched 2026-06-11. Code citations are from read-only clones under `/tmp/sessions-research/` (browser, device_detector, hotwire-native-ios, hotwire-native-android, turbo-rails, rails-stable @ v8.1.3) and the local apps under `/Users/javi/GitHub/`. Web citations carry URL + fetch date.
4
+
5
+ ## Top findings
6
+
7
+ - **Both Ruby UA parsers are zero-dependency, but neither is fully alive.** `browser` (MIT): last release 6.2.0 on 2024-12-04, last commit 2025-06-10. `device_detector` (LGPL-3.0): last commit/release 1.1.3 on **2024-07-03** — its vendored Matomo data is frozen at June 2024 (zero `iPhone17,x` identifiers, barely knows Pixel 9). Staleness matters less than it sounds because reduced web UAs carry no device models anyway — but it kills device_detector's main selling point.
8
+ - **device_detector's cache is not an LRU.** It's a Mutex-guarded bounded Hash (default 5,000 keys) that evicts the first-inserted third when full (`lib/device_detector/memory_cache.rb:5-60`).
9
+ - **Every prior-art gem stores the raw UA and parses nothing** (authie even truncates to 255 chars — a footgun: Hotwire Native UAs with bridge-components easily exceed 255). Store raw `text`, parse into derived columns, keep re-parseability.
10
+ - **Hotwire Native UA construction is fully deterministic and documented in source**: iOS appends `"[prefix] Hotwire Native iOS; Turbo Native iOS; bridge-components: [...]"` via `applicationNameForUserAgent`; Android *prepends* `"[prefix] Hotwire Native Android; Turbo Native Android; bridge-components: [...];"` to the WebView's default Chromium UA. Neither embeds app version, SDK version, or (on iOS) device model.
11
+ - **Android WebView is explicitly excluded from Chrome's UA reduction** ("We don't have current plans for User-Agent Reduction on iOS and Android WebView" — chromium.org/updates/ua-reduction, fetched 2026-06-10). So Hotwire Native **Android** UAs still carry real device model + real Android version. Hotwire Native **iOS** UAs carry real iOS version but never the hardware model.
12
+ - **Web Chrome UAs are husks since 2023**: frozen `Windows NT 10.0`, `Intel Mac OS X 10_15_7`, `Linux; Android 10; K`, minor versions `0.0.0`. Real data moved to UA Client Hints — which **only Chromium ships; Safari and Firefox still refuse as of June 2026**.
13
+ - **iPadOS masquerades as macOS by default** since iPadOS 13 — server-side, an iPad on Safari is byte-identical to a Mac. No fix without JS.
14
+ - **Our four local apps customize the UA prefix today but none embeds app version or (iOS) device model** — the gem should ship a recommended prefix convention; CarHey's native HTTP client already proves the pattern (`"CarHey Android 1.0.5 (build 6; Android 14; sdk 34; Pixel 7)"`).
15
+ - **`request.remote_ip` behind Cloudflare returns a Cloudflare edge IP** unless CF ranges are added to `trusted_proxies` (which *replaces* the private-range defaults) — document `cloudflare-rails` or an `ip_resolver` hook. Portable IP column: `string limit: 45`; `inet` is Postgres-only.
16
+
17
+ ## A. Ruby UA parsers: `browser` vs `device_detector`
18
+
19
+ ### Side-by-side
20
+
21
+ | | `browser` (fnando) | `device_detector` (podigee) |
22
+ |---|---|---|
23
+ | Approach | Hand-rolled matcher classes, ordered list (`lib/browser/browser.rb:70-104`); first match wins (`:107-113`) | Ruby port of Matomo's device-detector; regex DB in YAML |
24
+ | Data size | ~15 KB YAML (bots.yml 327 lines, samsung.yml, languages.yml) | **1.5 MB** `regexes/` — mobiles.yml 1.1 MB, bots.yml 108 KB, client/browsers.yml 70 KB, mobile_apps.yml 60 KB |
25
+ | Browser name/version | `name`, `full_version`, `version` (major) | `name`, `full_version` |
26
+ | OS name/version | `platform.name`, `platform.version`, `ios?/android?/mac?/windows?` predicates | `os_name`, `os_full_version`, `os_family` |
27
+ | Device type | Predicates only: `device.mobile?/tablet?/console?` | `device_type` → smartphone/tablet/desktop/tv/wearable/console/feature phone/… (`lib/device_detector.rb:94-189`) |
28
+ | Device model | Named devices only (iPhone/iPad/PS/Xbox/Switch/Kindle/Surface) + Samsung via samsung.yml; **no generic Android model** | `device_name` + `device_brand` from mobiles.yml (full Matomo model DB) |
29
+ | Bot detection | `bot?`, `bot.name`, `bot.why?`, `search_engine?` — 327-line list | `bot?`, `bot_name` — 108 KB list incl. AI crawlers (as of 2024-06) |
30
+ | Client Hints | None | Yes: `DeviceDetector.new(ua, headers)` reads `Sec-CH-UA*` (`lib/device_detector/client_hint.rb:176-230`) |
31
+ | Caching | None global; per-instance memoization; UA length capped at 2048 (`lib/browser/browser.rb:62-66`) | Global `MemoryCache`: bounded Hash, 5,000 keys default, Mutex, evicts oldest ⅓ — **not LRU** (`memory_cache.rb:5-60`); configurable `max_cache_keys` |
32
+ | Runtime deps | **Zero** | **Zero** |
33
+ | License | **MIT** | **LGPL-3.0** (fine as a dependency, but some corp policies flag it) |
34
+ | Ruby | ≥ 3.2 | ≥ 2.7.5 |
35
+ | Last release | v6.2.0, 2024-12-04 (git tag) | v1.1.3, 2024-07-03 (git tag) |
36
+ | Last commit | 2025-06-10 (`git log`, clone) | **2024-07-03** (`git log`, `develop` = HEAD) |
37
+ | Rails sugar | `Browser::ActionController` helper, middleware, meta tags | None |
38
+
39
+ ### Notable internals
40
+
41
+ - `browser` detection is code, not data: e.g. Safari requires the literal `"Safari"` token and ~16 negative checks (`lib/browser/safari.rb:21-38`); iOS version comes from `OS (\d+)_(\d+)` (`lib/browser/platform/ios.rb:7-8`) and `platform.name` returns `"iOS (iPhone)"` (`ios.rb:26-28`). Webview heuristics ship built-in: `ios_app?` = iOS && no `"Safari"` token, `android_app?` = `/\bwv\b/` (`lib/browser/platform.rb:127-141`).
42
+ - `device_detector` is Client-Hints-native to a surprising degree: it **reconstructs reduced UAs**, substituting the frozen `"Android 10; K"` with the hinted model + `Sec-CH-UA-Platform-Version` before parsing (`lib/device_detector.rb:30-39`), reads `Sec-CH-UA`/`Sec-CH-UA-Full-Version-List` for browser identity (`client_hint.rb:176-210`) and `x-requested-with` for Android app names (`client_hint.rb:140-146`). Caveat: it expects literal header keys (`'Sec-CH-UA'`), not Rack-env `HTTP_SEC_CH_UA` — the caller must normalize.
43
+ - Data staleness check (clone, 2026-06-11): `grep -c "iPhone17\|iPhone16" regexes/device/mobiles.yml` → **0**; `grep -c "Pixel 9"` → 2. Two years of hardware missing. (Matomo upstream is alive; the Ruby port simply hasn't synced since 2024-07.)
44
+ - On a Hotwire Native iOS UA (no `Safari` token): `browser` yields Unknown browser + `platform.ios?` true + `ios_app?` true — platform usable, name useless. device_detector does no better; **neither understands `bridge-components:` or app prefixes**. Native parsing must be ours.
45
+
46
+ ### What other auth/session gems do (clones at /tmp/sessions-research/)
47
+
48
+ - **authie**: raw UA, truncated — `self.user_agent = user_agent[0, 255]` (`lib/authie/session_model.rb:119`).
49
+ - **authtrail**: raw `request.user_agent` (`lib/authtrail.rb:41`); geolocation via optional `geocoder` gem in a background job (README.md:112-131).
50
+ - **authentication-zero**: generates `t.string :user_agent` columns + `Current.user_agent = request.user_agent`; renders the raw string in views.
51
+ - **devise / warden / rodauth**: store nothing UA-related.
52
+ - Conclusion: nobody in the ecosystem turns a UA into "Chrome 137 on macOS" — that's the gap, and it validates **storing the raw UA always** (everyone does) while differentiating on parsed presentation.
53
+
54
+ ### Recommendation
55
+
56
+ 1. **Always persist the raw UA in a `text` column** (no 255 limit) plus the relevant raw Client-Hint headers when present. Parsing is a *projection* that can be re-run as parsers/conventions improve. Validated by uniform prior art.
57
+ 2. **Hard dependency on `browser`** as the default web parser: MIT, zero-dep, tiny, Rails-aware, good enough for "Chrome 137 on macOS" + bot flagging. A drop-in gem needs device intel working with zero setup; an adapter-only design would gut the first-run experience.
58
+ 3. **Optional `device_detector` adapter** (auto-upgrade if the host app bundles it): better device names on legacy/Android UAs, native Client-Hints handling, much bigger bot DB. Don't hard-depend: LGPL, 1.5 MB data, 2-years-stale releases.
59
+ 4. **Built-in native-app UA parser that runs first** (before any web parser): recognizes `Hotwire Native iOS|Android`, `Turbo Native`, the recommended prefix convention below, and CarHey's existing shapes. This is the gem's actual moat; no third-party parser does it.
60
+ 5. Expose `config.ua_parser = :browser | :device_detector | ->(ua, headers) { DeviceInfo.new(...) }` for escape hatches, and stamp rows with parser identity/version if cheap (optional).
61
+
62
+ ## B. Hotwire Native user agents
63
+
64
+ ### iOS construction (hotwire-native-ios, HEAD 2025-11-06, latest tag 1.3.0-beta)
65
+
66
+ `Source/Bridge/UserAgent.swift:3-15` — the literal composition:
67
+
68
+ ```swift
69
+ enum UserAgent {
70
+ static func build(applicationPrefix: String?, componentTypes: [BridgeComponent.Type]) -> String {
71
+ let components = componentTypes.map { $0.name }.joined(separator: " ")
72
+ let componentsSubstring = "bridge-components: [\(components)]"
73
+ return [applicationPrefix, "Hotwire Native iOS;", "Turbo Native iOS;", componentsSubstring]
74
+ .compactMap { $0 }.joined(separator: " ")
75
+ }
76
+ }
77
+ ```
78
+
79
+ Applied as the WebKit *application name*, not the whole UA: `configuration.applicationNameForUserAgent = userAgent` (`Source/HotwireConfig.swift:133`; `userAgent` property at `:47-55`; customization point `applicationUserAgentPrefix` at `:17`). WebKit then composes the final UA, replacing the default trailing `Mobile/15E148` segment. Official docs example for an iPhone on iOS 18.2 (https://native.hotwired.dev/ios/configuration, fetched 2026-06-10):
80
+
81
+ ```text
82
+ Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) My Application; Hotwire Native iOS; Turbo Native iOS; bridge-components: [form menu]
83
+ ```
84
+
85
+ Present by default: device *family* (`iPhone`/`iPad` — WKWebView does **not** masquerade like desktop-mode Safari), **real iOS version** (`18_2`), framework markers, bridge component names. Absent: hardware model (`iPhone15,2`), app name/version (unless prefix set), SDK version. Note: no `Safari` token, no `Version/x` token → generic web parsers see "unknown browser on iOS".
86
+
87
+ ### Android construction (hotwire-native-android, HEAD 2026-05-14, latest tag 1.2.8)
88
+
89
+ `core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt:83-94`:
90
+
91
+ ```kotlin
92
+ val userAgent: String get() {
93
+ val components = registeredBridgeComponentFactories.joinToString(" ") { it.name }
94
+ return listOf(
95
+ applicationUserAgentPrefix,
96
+ "Hotwire Native Android; Turbo Native Android;",
97
+ "bridge-components: [$components];"
98
+ ).filterNotNull().joinToString(" ")
99
+ }
100
+ ```
101
+
102
+ …and `userAgentWithWebViewDefault` (`HotwireConfig.kt:100-102`) = `"$userAgent ${Hotwire.webViewInfo(context).defaultUserAgent}"`, i.e. the Hotwire segment comes **before** the stock Chromium UA (`WebSettings.getDefaultUserAgent`, `WebViewInfo.kt:42`), set on every WebView at `HotwireWebView.kt:42`. Customization: `applicationUserAgentPrefix` (`HotwireConfig.kt:74`). Docs: https://native.hotwired.dev/android/configuration (fetched 2026-06-10). Resulting shape:
103
+
104
+ ```text
105
+ MyApp; Hotwire Native Android; Turbo Native Android; bridge-components: [form menu]; Mozilla/5.0 (Linux; Android 16; Pixel 8 Build/BP2A…; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/137.0.0.0 Mobile Safari/537.36
106
+ ```
107
+
108
+ Present by default: **real Android version, real device model, Build id** (WebView is exempt from UA reduction — see §C), Chrome major version, `wv` webview token, framework markers. Absent: app name/version, SDK version.
109
+
110
+ ### turbo-rails detection helper (turbo-rails, HEAD 2026-01-29, v2.0.23)
111
+
112
+ `app/controllers/turbo/native/navigation.rb:13-19`:
113
+
114
+ ```ruby
115
+ # Hotwire Native applications are identified by having the string "Hotwire Native" as part of their user agent.
116
+ def hotwire_native_app?
117
+ request.user_agent.to_s.match?(/(Turbo|Hotwire) Native/)
118
+ end
119
+ alias_method :turbo_native_app?, :hotwire_native_app?
120
+ ```
121
+
122
+ Substring contract: `"Turbo Native"` or `"Hotwire Native"` anywhere in the UA. The SDKs append these automatically after any prefix, so prefixes can't break it. The sessions gem should match the same regex for platform classification, then refine `iOS` vs `Android` from the following token.
123
+
124
+ ### What our local apps send today (greps 2026-06-11)
125
+
126
+ | App | Prefix set | Where | Effective UA shape |
127
+ |---|---|---|---|
128
+ | carhey-ios | `"CarHey iOS; RailsFast Native iOS;"` (`{App}` from `CFBundleDisplayName`) | `RailsFast/Core/AppConfiguration.swift:10-12`, applied `RailsFast/App/AppDelegate.swift:103` | `Mozilla/5.0 (iPhone; CPU iPhone OS x_y like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CarHey iOS; RailsFast Native iOS; Hotwire Native iOS; Turbo Native iOS; bridge-components: […]` |
129
+ | railsfast-ios | `"RailsFast iOS; RailsFast Native iOS;"` | `RailsFast/Core/AppConfiguration.swift:10-12`, `RailsFast/App/AppDelegate.swift:93-99` | same shape, `RailsFast` tokens |
130
+ | carhey-android | `"CarHey Android;"` | `app/src/main/java/com/carhey/android/CarHeyApplication.kt:169` | `CarHey Android; Hotwire Native Android; Turbo Native Android; bridge-components: […]; Mozilla/5.0 (Linux; Android NN; <Model> Build/…; wv) … Chrome/NNN.0.0.0 Mobile Safari/537.36` |
131
+ | railsfast-android | `"${BuildConfig.APPLICATION_NAME} Android; RailsFast Native Android;"` | `app/src/main/java/com/railsfast/android/RailsFastApplication.kt:45-46` | same shape with two leading brand segments |
132
+
133
+ So today: **no webview UA carries the app version anywhere, and iOS UAs carry no device model.** Android model/OS arrive free via the WebView default UA.
134
+
135
+ However, CarHey's *native* (URLSession/OkHttp) calls already use a richer convention the gem should accept as prior art:
136
+
137
+ - iOS: `"\(applicationName) iOS \(version) (build \(build); iOS \(osVersion); \(resolvedModel))"` → e.g. `CarHey iOS 1.0.5 (build 6; iOS 19.5; iPhone15,2)` (`carhey-ios/RailsFast/Core/NativeHttpClient.swift:61-71`), plus headers `X-Client-Platform/-Version/-Build/-OS` (`NativeHttpClient.swift:13-17`).
138
+ - Android: `"CarHey Android $versionName (build $versionCode; Android $osRelease; sdk $sdkInt; $device)"` → e.g. `CarHey Android 1.0.5 (build 6; Android 14; sdk 34; Pixel 7)` (`carhey-android/app/src/main/java/com/carhey/android/ClientHeaders.kt:66-76`; header names `:25-29`).
139
+
140
+ ### Recommended UA convention for the gem's README
141
+
142
+ Use an RFC 9110-style product token as the `applicationUserAgentPrefix`, ending with `;` so it reads cleanly before the SDK-appended segments:
143
+
144
+ ```text
145
+ <AppName>/<version> (<model>; <os> <os_version>; build <build>);
146
+ e.g. CarHey/2.4.1 (iPhone15,2; iOS 19.5; build 241);
147
+ e.g. CarHey/2.4.1 (Pixel 8; Android 16; build 241);
148
+ ```
149
+
150
+ Parse rule (gem-side, tolerant): `%r{(?<app>[\w .-]+)/(?<version>\d[\w.]*) \((?<fields>[^)]*)\)}` with semicolon-split, order-insensitive fields; also accept CarHey's space-separated legacy `"CarHey iOS 1.0.5 (build 6; …)"`. Everything else (Hotwire markers, WebView UA) stays intact, so `hotwire_native_app?` and bridge components keep working.
151
+
152
+ README client snippets:
153
+
154
+ **iOS (AppDelegate, before creating the Navigator):**
155
+
156
+ ```swift
157
+ var u = utsname(); uname(&u)
158
+ let model = withUnsafeBytes(of: &u.machine) { String(decoding: $0.prefix(while: { $0 != 0 }), as: UTF8.self) }
159
+ Hotwire.config.applicationUserAgentPrefix = "CarHey/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0") (\(model); iOS \(UIDevice.current.systemVersion));"
160
+ ```
161
+
162
+ (`model` is `"iPhone15,2"` on device, `"arm64"` on Simulator — fine for production traffic.)
163
+
164
+ **Android (Application.onCreate, before any HotwireActivity):**
165
+
166
+ ```kotlin
167
+ Hotwire.config.applicationUserAgentPrefix =
168
+ "CarHey/${BuildConfig.VERSION_NAME} " +
169
+ "(${Build.MODEL}; Android ${Build.VERSION.RELEASE}; build ${BuildConfig.VERSION_CODE});"
170
+ ```
171
+
172
+ Even without these snippets the gem still detects platform + (Android) model + (iOS) OS version from the defaults; the snippets add app version everywhere and hardware model on iOS.
173
+
174
+ ## C. Web platform realities, June 2026
175
+
176
+ ### Chrome UA reduction — long finished, fully frozen
177
+
178
+ Complete since Chrome 110-113 (2023). Final templates (https://www.chromium.org/updates/ua-reduction/, fetched 2026-06-10):
179
+
180
+ - Desktop: `Mozilla/5.0 (<unifiedPlatform>) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<major>.0.0.0 Safari/537.36`
181
+ - Mobile/tablet: `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<major>.0.0.0 [Mobile ]Safari/537.36`
182
+
183
+ Frozen literals: `Windows NT 10.0; Win64; x64` (Win10 vs Win11 indistinguishable), `Macintosh; Intel Mac OS X 10_15_7` (even Apple Silicon), `Android 10`, model `K`, minor version `0.0.0`. Variable: Chrome major + `Mobile` token. **WebView exempt**: "We don't have current plans for User-Agent Reduction on iOS and Android WebView at this time" (same page) — the load-bearing fact for Hotwire Native Android.
184
+
185
+ Safari freezes macOS at `10_15_7` too but reports **real iOS versions** on iPhone (`iPhone; CPU iPhone OS 26_0 like Mac OS X` style; see https://nielsleenheer.com/articles/2025/the-user-agent-string-of-safari-on-ios-26-and-macos-26/, 2025, found 2026-06-11). Firefox likewise caps macOS at `10.15` but otherwise sends real versions. Practical: from a 2026 web UA you can trust browser name + major version, OS *family*, mobile-vs-not — and almost nothing else.
186
+
187
+ ### UA Client Hints — the only way back to detail, Chromium-only
188
+
189
+ Mechanics (https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Client_hints, fetched 2026-06-11):
190
+
191
+ - **Low-entropy, sent by default on every secure request**: `Sec-CH-UA` (brand list + major), `Sec-CH-UA-Mobile` (`?1`/`?0`), `Sec-CH-UA-Platform` (`"Windows"`, `"macOS"`, `"Android"`, …), plus `Save-Data`.
192
+ - **High-entropy, server opt-in**: respond with `Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model, Sec-CH-UA-Full-Version-List` (also available: `-Arch`, `-Bitness`, `-Form-Factors`). The browser attaches them to **subsequent** requests only (second-request problem); `Critical-CH` triggers a transparent retry; add `Vary` on hint headers for cacheable responses.
193
+ - Yields: real macOS/Windows platform versions (`Sec-CH-UA-Platform-Version` ≥ `13.0.0` on Windows = Win11), Android **device model** (`Sec-CH-UA-Model`, empty on desktop), exact browser build (`Sec-CH-UA-Full-Version-List`).
194
+ - **Support matrix (June 2026): Chromium only** — Chrome/Edge/Opera/Brave/etc. **Safari: no. Firefox: no** (Mozilla position negative; https://caniuse.com/wf-ua-client-hints and https://github.com/mozilla/standards-positions/issues/552, checked 2026-06-10; corroborated by https://www.corbado.com/blog/client-hints-user-agent-chrome-safari-firefox).
195
+ - Lucky break for a *sessions* gem: login POSTs are never the first request of a browsing session, so if the app has been emitting `Accept-CH`, high-entropy hints are reliably present exactly when sessions get created.
196
+
197
+ ### iPadOS masquerades as macOS
198
+
199
+ Since iPadOS 13, Safari defaults to "Request Desktop Website" and sends the literal macOS UA `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 …` — no `iPad` token, no client hints, **no server-side tell** (https://developer.apple.com/forums/thread/119186 and nielsleenheer.com article above, checked 2026-06-11). Only JS (`navigator.maxTouchPoints > 1` on "MacIntel") can distinguish. PRD expectation: iPads will display as "Safari on macOS"; offer an optional JS beacon later if it matters.
200
+
201
+ ### Honest "device name" precision per platform (what the PRD should promise)
202
+
203
+ | Source | Can say | Cannot say |
204
+ |---|---|---|
205
+ | Chrome/Edge desktop | "Chrome 137 on Windows / macOS / Linux"; +Win11/real macOS version *with Accept-CH* | OS version from UA alone (frozen tokens — never render "macOS 10.15" or "Windows 10" as fact) |
206
+ | Chrome Android | "Chrome 137 on Android (phone/tablet)"; +model ("Pixel 8") and real Android version *with Accept-CH* | model/version from UA alone (`Android 10; K` is a lie) |
207
+ | Safari iPhone | "Safari on iOS 19.5, iPhone" (real OS version) | hardware model (never present) |
208
+ | Safari iPad/Mac | "Safari on macOS" | iPad vs Mac; any real macOS version |
209
+ | Firefox | "Firefox 1xx on <OS family>" | real macOS version; any hints |
210
+ | Hotwire Native iOS | app name + platform + real iOS version (+ model, app version *with our prefix convention*) | model without the convention |
211
+ | Hotwire Native Android | app name + real Android version + real model + Chrome version (+ app version *with convention*) | — |
212
+
213
+ ## D. IP capture correctness
214
+
215
+ ### `ActionDispatch::RemoteIp` semantics (rails-stable v8.1.3)
216
+
217
+ - Algorithm (`actionpack/lib/action_dispatch/middleware/remote_ip.rb:129-169`): collect `Client-Ip` + `X-Forwarded-For` (reversed, i.e. nearest-first), validate each entry (`sanitize_ips` rejects netmasks/garbage, `:185-196`), spoof-check (`IpSpoofAttackError` if `Client-Ip` and XFF disagree, `:150-153`), then `filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr` (`:169`) — i.e. **the closest-to-client address that is not a trusted proxy**.
218
+ - Default `TRUSTED_PROXIES` (`remote_ip.rb:40-49`): loopback, RFC 1918 ranges, link-local, `fc00::/7`.
219
+ - `config.action_dispatch.trusted_proxies` **replaces** the default list ("will be used *instead of* `TRUSTED_PROXIES`", `:61-62`; `@proxies = custom_proxies || TRUSTED_PROXIES`, `:70-74`) — when adding CDN ranges you must re-include the private ranges if an LB/private hop also sets XFF. Single values raise `ArgumentError` (`:75-80`).
220
+ - Use `request.remote_ip` (this middleware), never `request.ip` (Rack's looser logic) and never raw `X-Forwarded-For`.
221
+
222
+ ### Cloudflare (RailsFast deploys behind it)
223
+
224
+ - Cloudflare *appends* to inbound `X-Forwarded-For` and recommends origins read **`CF-Connecting-IP`** ("CF-Connecting-IP provides the client IP address connecting to Cloudflare to the origin web server"; "Cloudflare recommends that your logs or applications look at CF-Connecting-IP or True-Client-IP instead of X-Forwarded-For" — https://developers.cloudflare.com/fundamentals/reference/http-headers/, fetched 2026-06-11). `True-Client-IP` is Enterprise-only and identical in content.
225
+ - Problem: Cloudflare edge IPs are **public**, so with default trusted proxies `remote_ip` returns the CF edge address, not the visitor. Fixes, best-first:
226
+ 1. `cloudflare-rails` gem — fetches CF's published IP ranges on boot and folds them into the trusted-proxy logic so `request.remote_ip` just works.
227
+ 2. Manual: `config.action_dispatch.trusted_proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES + CF_IPV4_RANGES + CF_IPV6_RANGES` (remember: replacement semantics).
228
+ 3. Read `CF-Connecting-IP` directly **only** if the origin is unreachable except via Cloudflare (otherwise trivially spoofable by direct-to-origin requests).
229
+ - Gem stance: default to `request.remote_ip`; expose `config.ip_resolver = ->(request) { ... }` for CF-Connecting-IP setups; ship a "Behind Cloudflare" README section with the three options above. Never parse XFF ourselves.
230
+
231
+ ### Storage column across sqlite/mysql/pg
232
+
233
+ - `inet` is Postgres-only: registered at `activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:158` and `:1200` (`OID::Inet`, returns `IPAddr` objects); no `inet` mapping exists in the mysql or sqlite3 adapters.
234
+ - Portable choice for a gem: **`t.string :ip_address, limit: 45`** — 45 chars covers max IPv6 textual form including IPv4-mapped (`::ffff:255.255.255.255`). Normalize with `IPAddr.new(raw).to_s` before save (downcases, canonicalizes, rejects garbage). Optionally use `:inet` when `adapter_name == "PostgreSQL"` in the install generator for index/CIDR-query friendliness; keep model code `IPAddr`-agnostic since PG returns `IPAddr` and others return `String`.
235
+ - Privacy note for the README: IPs are personal data (GDPR); consider an opt-in anonymization mode (zero the last octet / last 80 bits) and document retention.
236
+
237
+ ## Implications for the sessions gem
238
+
239
+ **Parser strategy.** Three-layer pipeline, raw-first:
240
+ 1. Persist raw `user_agent` (text) and, when present, the interesting headers (`Sec-CH-UA`, `Sec-CH-UA-Mobile`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Platform-Version`, `Sec-CH-UA-Model`, `Sec-CH-UA-Full-Version-List`, `X-Client-*`) into a `client_hints` json/jsonb column. Re-parse is always possible; ship `sessions:reparse` task.
241
+ 2. Built-in **native matcher first**: `/(Turbo|Hotwire) Native (iOS|Android)/` (same contract as turbo-rails `navigation.rb:15-17`) → platform; then the prefix convention regex → app name/version/build/model/OS version; on Android fall back to the embedded WebView UA for model/OS.
242
+ 3. Web UAs → **`browser` gem as hard dependency** (MIT, zero-dep, ~15 KB data) for name/version/platform/bot; **`device_detector` as auto-detected optional adapter** (better device names, CH-aware, 108 KB bot list — but LGPL, 1.5 MB, data frozen 2024-07); `config.ua_parser` accepts a lambda for BYO.
243
+
244
+ **Schema (device fields on `sessions` / `login_activities`):** `user_agent :text`, `client_hints :json`, `browser_name :string`, `browser_version :string`, `os_name :string`, `os_version :string`, `device_type :string` (desktop/smartphone/tablet/native_ios/native_android/bot/unknown), `device_model :string`, `app_name :string`, `app_version :string`, `ip_address :string, limit: 45` (`:inet` on PG). Display name composes defensively: never render frozen tokens ("macOS 10.15", "Android 10", model "K") as facts.
245
+
246
+ **Client hints.** Optional `config.request_client_hints = true` → set `Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model, Sec-CH-UA-Full-Version-List` (HTTPS responses only; document `Vary` if responses cache). Works only on Chromium — Safari/Firefox sessions stay UA-only; the login request is rarely a first navigation, so hints are usually present when sessions are created.
247
+
248
+ **Native convention.** Ship the iOS/Android snippets from §B in the README; parse both the convention and Hotwire defaults so unconfigured apps still get platform + OS (+ Android model) for free.
249
+
250
+ **IP.** Default `request.remote_ip`; `config.ip_resolver` hook; "Behind Cloudflare" docs (cloudflare-rails / trusted_proxies-with-defaults / CF-Connecting-IP-if-locked); normalize via `IPAddr`; optional anonymization.
@@ -0,0 +1,216 @@
1
+ # The Rails 8+ auth landscape (2024–2026)
2
+
3
+ > Research memo for the `sessions` gem PRD. Compiled 2026-06-11 via live web research.
4
+ > Every claim carries a URL and date. Quotes are verbatim from primary sources unless marked otherwise.
5
+ > Items that could not be confirmed against a primary source are marked **UNVERIFIED**.
6
+
7
+ ---
8
+
9
+ ## Top findings
10
+
11
+ 1. **Rails 8.0 (released 2024-11-07) ships a first-party authentication *generator*, not an auth library.** `bin/rails generate authentication` emits ~12 files of plain app code: `User` + `Session` models, `SessionsController`, `PasswordsController`/`PasswordsMailer` (reset flow), and an `Authentication` concern ([rubyonrails.org, 2024-11-07](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required)).
12
+ 2. **Sessions are database-backed rows, not just cookies** — the generated `sessions` table stores `ip_address` and `user_agent` per session. Rails itself calls the result a "session-based, password-resettable, **metadata-tracking** authentication system" ([Rails 8.0 release notes](https://guides.rubyonrails.org/8_0_release_notes.html)). This is the exact substrate the `sessions` gem builds on.
13
+ 3. **But the generated `Session` model is two lines** — `belongs_to :user`, nothing else (verified on `rails/rails` main, fetched 2026-06-11: [session.rb.tt](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/authentication/templates/app/models/session.rb.tt)). No device naming, no last-seen tracking, no session listing UI, no "revoke other sessions". The metadata is captured and then **never surfaced**.
14
+ 4. **DHH scoped the generator deliberately small and said so on the PR**: "This is not intended to be an all-singing, all-dancing answer to every possible authentication concern… rolling your own authentication system is not some exotic adventure" and "do not expect magic links or passkeys or 2FA. That's not going to happen with this generator" ([PR #52328, merged 2024-07-16](https://github.com/rails/rails/pull/52328)).
15
+ 5. **Registration/sign-up is excluded by design**: "All you have to bring yourself is a user sign-up flow (since those are usually bespoke to each application)" ([Rails 8.0 announcement, 2024-11-07](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required)). Every tutorial ecosystem promptly grew "add sign-up to Rails 8 auth" posts.
16
+ 6. **Rails 8.1 (2025-10-22) and the in-progress 8.2 added essentially nothing to authentication** — 8.1's release notes contain zero auth/session entries (verified [8.1 release notes](https://guides.rubyonrails.org/8_1_release_notes.html)); 8.2 edge notes only add Argon2 for `has_secure_password` and `Sec-Fetch-Site` CSRF ([edge release notes, fetched 2026-06-10](https://edgeguides.rubyonrails.org/8_2_release_notes.html)). The generator's gaps are stable market territory, not a closing window.
17
+ 7. **Devise is not dying — it's growing.** Total downloads 280.87M; v5.0.4 shipped 2026-05-08 (RubyGems API, fetched 2026-06-10). Daily downloads in June 2026 peak at ~205–239k/weekday vs ~150–192k in June 2024 (BestGems API, fetched 2026-06-10). The realistic 2026 picture: two large installed bases (Devise + Rails 8 auth) that **both** lack session/device management UI.
18
+ 8. **Devise was simultaneously the most *loved* and most *frustrating* gem** in Planet Argon's 2024 Rails Community Survey (2,700+ respondents, 106 countries) ([railsdeveloper.com/survey/2024](https://railsdeveloper.com/survey/2024/), fetched 2026-06-10). The 2026 survey is open through 2026-07-03; results not yet published as of this memo ([railsdeveloper.com/survey](https://railsdeveloper.com/survey/)).
19
+ 9. **authentication-zero, the closest pre-Rails-8 "generated auth" gem, stalled** — last release 4.0.3 on 2024-10-26, ~19 months silent (RubyGems API, 2026-06-10): the framework absorbed its core value. Meanwhile **authtrail ("Track Devise login activity") sits at 4.11M downloads and hit 1.0.0 on 2026-04-04** — direct, quantified demand for login-activity tracking as an add-on.
20
+ 10. **The community's #1 documented post-generator chores**: sign-up/registration, OAuth/social login, magic links, email verification, test helpers — and *visible session management*. WorkOS's 2026 Rails auth guide explicitly frames the need: "You're logged in on iPhone, MacBook, and Windows PC – sign out others?" ([workos.com, 2026 guide](https://workos.com/blog/rails-authentication-guide-2026)).
21
+ 11. **DHH's stated philosophy is generated-owned-code over dependencies**: "Rails won't ship with Devise, but it will generate authentication code for you… The code is an extraction from 37signals' apps" (Rails World 2024 keynote, 2024-09-26, via [Kyrylo Silin's notes, 2024-09-27](https://kyrylo.org/rails/2024/09/27/notes-from-the-opening-keynote-by-david-heinemeier-hansson-at-rails-world-2024.html)). A gem that *adds* device/session UX on top of owned auth code aligns with the doctrine instead of fighting it.
22
+ 12. **There is still no dedicated official authentication guide** at guides.rubyonrails.org as of June 2026 — the generator is documented in a short section of the Securing Rails Applications guide ([security guide](https://guides.rubyonrails.org/security.html)); devs literally asked "Rails 8 Authentication generator docs?" on the official forum ([discuss.rubyonrails.org](https://discuss.rubyonrails.org/t/rails-8-authentication-generator-docs/87905)).
23
+ 13. **Session-tracking how-tos exist for both stacks and predate/postdate Rails 8** — SupeRails built "manage active sessions" for Devise (2024-03-24) and "Devise has_many :sessions — track, list, and revoke" via Warden hooks; a 2025-era Substack series does multi-device session tracking for Devise. People keep re-building this by hand. (URLs in §7.)
24
+ 14. **Rails World 2025 (Amsterdam, 2025-09-04/05) announced no new auth features** — keynote themes were Rails 8.1 beta, "Pax Railsana", Omarchy ([Andy Croll recap](https://andycroll.com/ruby/rails-world-2025/), [Kevin McKelvin recap, 2025-09](https://kmckelvin.com/blog/2025/09/rails-world-2025/)). Rails World 2026 is Austin, TX, 2026-09-23/24 ([rubyonrails.org](https://rubyonrails.org/2026/3/24/Rails-Versions-8-0-5-and-8-1-3-have-been-released) era announcements).
25
+ 15. **The generated controller already rate-limits sign-in** (`rate_limit to: 10, within: 3.minutes, only: :create`) — verified verbatim from the template on rails/rails main (fetched 2026-06-11). Rails handles abuse at the door; it does nothing about *visibility* of what's inside.
26
+
27
+ ---
28
+
29
+ ## Timeline
30
+
31
+ | Date | Event | Source |
32
+ |---|---|---|
33
+ | 2023-12-26 | DHH opens issue **#50446 "Add basic authentication generator"**: gems that hide mechanics "should not be seen as a necessity" | [github.com/rails/rails/issues/50446](https://github.com/rails/rails/issues/50446) |
34
+ | 2024-07-16 | DHH's **PR #52328 "Add basic sessions generator"** merged (later renamed `authentication` generator); excludes magic links/passkeys/2FA by fiat | [github.com/rails/rails/pull/52328](https://github.com/rails/rails/pull/52328) |
35
+ | 2024-09-26/27 | **Rails World 2024** (Toronto). Opening keynote announces Rails 8 beta incl. authentication generator. Video: [youtube.com/watch?v=-cEn_83zRFw](https://www.youtube.com/watch?v=-cEn_83zRFw); official page: [rubyonrails.org/world/2024/day-1/opening-keynote-dhh](https://rubyonrails.org/world/2024/day-1/opening-keynote-dhh) | keynote notes: [kyrylo.org, 2024-09-27](https://kyrylo.org/rails/2024/09/27/notes-from-the-opening-keynote-by-david-heinemeier-hansson-at-rails-world-2024.html) |
36
+ | 2024-09-27 | **"Rails 8.0 Beta 1: No PaaS Required"** — section *"Generating the authentication basics"* | [rubyonrails.org/2024/9/27/rails-8-beta1-no-paas-required](https://rubyonrails.org/2024/9/27/rails-8-beta1-no-paas-required) |
37
+ | ~2024-10-22 | HN front-page thread **"Rails 8 Authentication Generator"** | [news.ycombinator.com/item?id=41922905](https://news.ycombinator.com/item?id=41922905) (date inferred from item ID; thread content UNVERIFIED — HN rate-limited fetch) |
38
+ | 2024-11-07 | **Rails 8.0 final: "Rails 8.0: No PaaS Required"** (author: dhh) | [rubyonrails.org/2024/11/7/rails-8-no-paas-required](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required) |
39
+ | 2024-11-07/08 | DHH on X: "Rails 8.0: #NOBUILD, #NOPAAS, … **new authentication generator**, and so much more! Final release is out 🎉" | [x.com/dhh/status/1854659013604262345](https://x.com/dhh/status/1854659013604262345) (date inferred from ID) |
40
+ | 2024-11-17 | Community proposal: **"Authentication via magic links"** generator — no core-team adoption | [discuss.rubyonrails.org/t/87944](https://discuss.rubyonrails.org/t/proposal-authentication-via-magic-links/87944) |
41
+ | 2024-12-13 | Rails 8.0.1; official **"Want to learn about Rails 8? START HERE"** tutorial push | [rubyonrails.org/2024/12/13/learn-Rails-8-tutorial-and-unpacked-videos](https://rubyonrails.org/2024/12/13/learn-Rails-8-tutorial-and-unpacked-videos) |
42
+ | 2025-09-04 | **Rails 8.1 Beta 1** (announced around Rails World 2025, Amsterdam 2025-09-04/05) | [rubyonrails.org/2025/9/4/rails-8-1-beta-1](https://rubyonrails.org/2025/9/4/rails-8-1-beta-1) |
43
+ | 2025-10-22 | **Rails 8.1 final: "Job continuations, structured events, local CI"** (author: rafaelfranca). **No auth/session features** | [rubyonrails.org/2025/10/22/rails-8-1](https://rubyonrails.org/2025/10/22/rails-8-1) |
44
+ | 2025-10-29 | New releases + **end-of-support announcement** | [rubyonrails.org/2025/10/29/new-rails-releases-and-end-of-support-announcement](https://rubyonrails.org/2025/10/29/new-rails-releases-and-end-of-support-announcement) |
45
+ | 2026-03-24 | Rails **8.0.5 / 8.1.3** bugfix releases; 8.1 series gets bug fixes until Oct 2026; 8.0 moves to security-only May 2026 | [rubyonrails.org/2026/3/24/Rails-Versions-8-0-5-and-8-1-3-have-been-released](https://rubyonrails.org/2026/3/24/Rails-Versions-8-0-5-and-8-1-3-have-been-released) |
46
+ | 2026 (in progress) | **Rails 8.2 edge release notes**: Argon2 option for `has_secure_password`, `Sec-Fetch-Site` CSRF. No release date stated | [edgeguides.rubyonrails.org/8_2_release_notes.html](https://edgeguides.rubyonrails.org/8_2_release_notes.html) (fetched 2026-06-10) |
47
+ | 2026-09-23/24 | **Rails World 2026**, Austin, TX (announcements expected) | via search results incl. [rubyonrails.org blog](https://rubyonrails.org/blog/), fetched 2026-06-10 |
48
+
49
+ Key release-notes language (verbatim, [Rails 8.0 release notes §2.7](https://guides.rubyonrails.org/8_0_release_notes.html)):
50
+
51
+ > "[Authentication system generator](https://github.com/rails/rails/pull/52328), creates a starting point for a session-based, password-resettable, metadata-tracking authentication system."
52
+
53
+ Rails 8.1 release notes ([guides.rubyonrails.org/8_1_release_notes.html](https://guides.rubyonrails.org/8_1_release_notes.html), fetched 2026-06-10): **no authentication, session, or cookie entries at all** (verified by full-document review).
54
+
55
+ ---
56
+
57
+ ## What Rails ships vs excludes
58
+
59
+ ### Ships (Rails 8.0 generator, verified against rails/rails `main` 2026-06-11)
60
+
61
+ - `User` model with `has_secure_password` (bcrypt), uniquely-indexed `email_address` ([BigBinary, 2024](https://www.bigbinary.com/blog/rails-8-introduces-a-basic-authentication-generator)).
62
+ - **`Session` model backed by a `sessions` table with `token`, `ip_address`, `user_agent`** — "the sessions table … stores the `ip_address` and `user_agent` information for every session started by the `User`" ([Avo blog, 2025-01-27](https://avohq.io/blog/rails-8-authentication)). The model itself is minimal — verbatim, entire file ([session.rb.tt](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/authentication/templates/app/models/session.rb.tt)):
63
+ ```ruby
64
+ class Session < ApplicationRecord
65
+ belongs_to :user
66
+ end
67
+ ```
68
+ - `SessionsController` with built-in throttling — verbatim from the template ([sessions_controller.rb.tt](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/authentication/templates/app/controllers/sessions_controller.rb.tt), fetched 2026-06-11):
69
+ ```ruby
70
+ allow_unauthenticated_access only: %i[ new create ]
71
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
72
+ ```
73
+ - `Authentication` concern (`start_new_session_for`, `terminate_session`, `Current.session`, signed permanent cookie), `PasswordsController` + `PasswordsMailer` (reset flow), `Current` attributes class, Action Cable connection auth (follow-up [PR #53444](https://github.com/rails/rails/pull/53444) by DHH). Component list: [Avo, 2025-01-27](https://avohq.io/blog/rails-8-authentication); [Saeloun, 2025-05-12](https://blog.saeloun.com/2025/05/12/rails-8-adds-built-in-authentication-generator/).
74
+ - Official docs: a section in the **Securing Rails Applications** guide — "Starting with version 8.0, Rails comes with a default authentication generator" ([guides.rubyonrails.org/security.html](https://guides.rubyonrails.org/security.html), fetched 2026-06-10). **No dedicated auth guide exists** as of June 2026; community asked for docs on the forum ([discuss.rubyonrails.org/t/87905](https://discuss.rubyonrails.org/t/rails-8-authentication-generator-docs/87905)).
75
+
76
+ ### Deliberately excludes (with receipts)
77
+
78
+ | Excluded | Source & quote |
79
+ |---|---|
80
+ | **Registration / sign-up** | "All you have to bring yourself is a user sign-up flow (since those are usually bespoke to each application)." — [Rails 8.0 announcement, 2024-11-07](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required) (same line in [Beta 1 post, 2024-09-27](https://rubyonrails.org/2024/9/27/rails-8-beta1-no-paas-required)) |
81
+ | **Magic links, passkeys, 2FA** | "So do not expect magic links or passkeys or 2FA. That's not going to happen with this generator." — DHH, [PR #52328](https://github.com/rails/rails/pull/52328), 2024-07 |
82
+ | **OAuth / social login** | Never in scope; not mentioned in any official 8.x release note (verified 8.0/8.1/8.2 notes, 2026-06-10). Community fills the gap (e.g. HN: ["Social login with the Rails 8 auth generator"](https://news.ycombinator.com/item?id=43239888), ~2025-03, date inferred from ID) |
83
+ | **Magic-link generator proposal → no** | Proposal by Alexey Ivanov, 2024-11-17; no core-team adoption. Community response: "Rails Authentication generator wants to be minimal… I would rather a gem pool of different auth generators for rails" (Igbanam, 2024-11-24) — [discuss.rubyonrails.org/t/87944](https://discuss.rubyonrails.org/t/proposal-authentication-via-magic-links/87944) |
84
+ | **Feature requests generally** | "Not as an open invitation to feature requests… the core team will propose a solution first" — DHH, [issue #50446](https://github.com/rails/rails/issues/50446), 2023-12-26 |
85
+ | **Email verification, account lockout, phone auth** | Left to the developer — "leaving you with the task of building your sign-up flow and any other feature like social login, magic links login, phone authentication, account confirmation, account locking, etc." ([Saeloun, 2025-05-12](https://blog.saeloun.com/2025/05/12/rails-8-adds-built-in-authentication-generator/)) |
86
+ | **Session/device management UI** | Nothing generated: no sessions index, no per-device naming, no "sign out everywhere", no last-active tracking. The `ip_address`/`user_agent` columns are written once at sign-in and never displayed. (Verified: generated views are only `sessions/new` + passwords views — [Avo, 2025-01-27](https://avohq.io/blog/rails-8-authentication); template tree in [PR #52328](https://github.com/rails/rails/pull/52328).) |
87
+
88
+ ---
89
+
90
+ ## DHH & the omakase philosophy (quotes)
91
+
92
+ **The founding issue** — DHH, 2023-12-26 ([rails/rails#50446](https://github.com/rails/rails/issues/50446)), verbatim:
93
+
94
+ > "Rails now include all the key building blocks needed to do basic authentication, but many new developers are still uncertain of how to put them together, so they end up leaning on all-in-one gems that hide the mechanics. While these gems are great, and many people enjoy using them, they should not be seen as a necessity. We can teach Rails developers how to use the basic blocks by adding a basic authentication generator that essentially works as a scaffold, but for authentication."
95
+
96
+ **The PR** — DHH, merged 2024-07-16 ([rails/rails#52328](https://github.com/rails/rails/pull/52328)), verbatim:
97
+
98
+ > "Adds a basic sessions generator to get people started with their own authentication system. This is not intended to be an all-singing, all-dancing answer to every possible authentication concern. It's merely intended to illuminate the basic path, and reveal that rolling your own authentication system is not some exotic adventure."
99
+
100
+ > "So do not expect magic links or passkeys or 2FA. That's not going to happen with this generator."
101
+
102
+ **Rails World 2024 keynote** (Toronto, 2024-09-26; video [youtube.com/watch?v=-cEn_83zRFw](https://www.youtube.com/watch?v=-cEn_83zRFw)) — as recorded in [Kyrylo Silin's contemporaneous notes, 2024-09-27](https://kyrylo.org/rails/2024/09/27/notes-from-the-opening-keynote-by-david-heinemeier-hansson-at-rails-world-2024.html):
103
+
104
+ > "Rails won't ship with Devise, but it will generate authentication code for you."
105
+ > "The code is an extraction from 37signals' apps. You can learn it and level up."
106
+ > "The mission of Rails is to compress the complexity of modern web apps."
107
+
108
+ **On dependency-aversion and learned helplessness** — reported by The New Stack in "DHH Wants To Make Web Dev Easy Again, With Ruby on Rails" ([thenewstack.io](https://thenewstack.io/dhh-wants-to-make-web-dev-easy-again-with-ruby-on-rails/); quotes surfaced via search excerpts; full article fetch failed — **partially UNVERIFIED**):
109
+
110
+ > "You actually have to realize that authenticating a user is not worth being a pink elephant for — let alone paying someone else to do it. You should understand the basics of secure passwords."
111
+ > On the generator: "It's going to put you on the path of learning what the fuck is going on…"
112
+
113
+ **The doctrine frame** — [rubyonrails.org/doctrine](https://rubyonrails.org/doctrine) (fetched 2026-06-11). Pillar 3, "The menu is omakase":
114
+
115
+ > "How do you know what to order in a restaurant when you don't know what's good? Well, if you let the chef choose, you can probably assume a good meal, even before you know what 'good' is."
116
+
117
+ Auth-in-the-box is the omakase logic finally applied to authentication, 20 years in; the *generator* form (vs. a library) honors pillar 6, "Provide sharp knives" — code you own and may cut yourself on. Other pillars: Optimize for programmer happiness; Convention over Configuration; No one paradigm; Exalt beautiful code; Value integrated systems; Progress over stability; Push up a big tent.
118
+
119
+ **Rails World 2025 keynote** (Amsterdam, 2025-09-04; video [youtube.com/watch?v=gcwzWzC7gUA](https://www.youtube.com/watch?v=gcwzWzC7gUA); official page [rubyonrails.org/world/2025/day-1/david-hansson](https://rubyonrails.org/world/2025/day-1/david-hansson)): Rails 8.1 beta, Active Job Continuations, "Pax Railsana" golden-age framing, Omarchy demo — **no auth announcements** ([Andy Croll recap](https://andycroll.com/ruby/rails-world-2025/); [Kevin McKelvin recap, 2025-09](https://kmckelvin.com/blog/2025/09/rails-world-2025/)).
120
+
121
+ **Long-form interviews** (context, no auth quotes extracted — listed for completeness): Remote Ruby, "DHH on Rails World 2024 and what's coming in Rails 8.1" (~2024-10) — [podcasts.apple.com](https://podcasts.apple.com/us/podcast/dhh-on-rails-world-2024-and-whats-coming-in-rails-8-1/id1397042613?i=1000673050958); Changelog Interviews #615, "Rails is having a moment (again)" (~2024-11) — [changelog.com/podcast/615](https://changelog.com/podcast/615); Lex Fridman #474 transcript — [lexfridman.com/dhh-david-heinemeier-hansson-transcript](https://lexfridman.com/dhh-david-heinemeier-hansson-transcript/). Auth-specific content in these: **UNVERIFIED**.
122
+
123
+ ---
124
+
125
+ ## Community reception & tutorials
126
+
127
+ Reception in one line: enthusiastic about *owning* auth, immediately followed by a cottage industry of "here's what it doesn't do" tutorials.
128
+
129
+ **What tutorials consistently hand-build on top of the generator** (frequency-ranked from the corpus below): (1) registration/sign-up, (2) OAuth/social login, (3) magic links, (4) email verification/confirmation, (5) tests & test helpers, (6) **session listing / device management / revocation**, (7) impersonation, admin constraints.
130
+
131
+ | # | Source (date) | One-liner |
132
+ |---|---|---|
133
+ | 1 | [GoRails — "How To Add Impersonation To Rails Authentication Generator"](https://gorails.com/episodes/how-to-add-impersonation-to-rails-authentication-generator) | Extends generated `Session`/`Current` to impersonate users — proof the Session model is the extension point ([repo](https://github.com/gorails-screencasts/impersonation-rails-8-authentication-generator)) |
134
+ | 2 | [GoRails — "Authentication Generator Test Helpers"](https://gorails.com/episodes/authentication-generator-test-helpers) | New upstream test helpers + extending them to system tests |
135
+ | 3 | [GoRails — "Routing Constraints with Rails Authentication Generator"](https://gorails.com/episodes/routing-constraints-with-rails-authentication-generator) | Authenticated routing constraints on top of generated auth |
136
+ | 4 | [GoRails — "What's New in Rails 8.0" series](https://gorails.com/series/whats-new-in-rails-8) | Multi-episode Rails 8 coverage incl. auth generator walkthrough |
137
+ | 5 | [Rob Race — "Adding Sign Up to the Rails 8 Authentication Generator"](https://robrace.dev/blog/rails-8-authentication-sign-up/) | The canonical missing piece: registration |
138
+ | 6 | [Josef Strzibny — "Extending Rails authentication generator with registration flow"](https://nts.strzibny.name/rails-authentication-registrations/) | Same gap, by the *Deployment from Scratch* author |
139
+ | 7 | [dev.to/1klap — "Extending Rails 8 authentication with OAuth sign-in and the missing RSpec test suite"](https://dev.to/1klap/extending-the-ruby-on-rails-8-authentication-with-oauth-sign-in-and-the-missing-rspec-test-suite-4ia) | OAuth + tests, both absent from the generator |
140
+ | 8 | [Rails Designer — "Adding Magic Links to Rails 8 Authentication"](https://dev.to/railsdesigner/adding-magic-links-to-rails-8-authentication-151n) | Magic links bolted onto generated auth |
141
+ | 9 | [Radan Skorić (guest) — "Migrating from Devise to Rails Auth before you can say 'Rails World keynote'"](https://radanskoric.com/guest-articles/from-devise-to-rails-auth) | Real-world Devise → Rails 8 auth migration write-up |
142
+ | 10 | [Andrii Furmanets — "Built-In Authentication in Rails 8: Deep Dive and Comparison"](https://andriifurmanets.com/blogs/built-in-authentication-in-rails) | Notes the generator "keeps track of sessions history instead of just storing the latest session information like [Devise's] trackable module" — i.e. better raw data, still no UI |
143
+ | 11 | [BigBinary — "Rails 8 introduces a basic authentication generator"](https://www.bigbinary.com/blog/rails-8-introduces-a-basic-authentication-generator) | Schema-level walkthrough (`token`, `ip_address`, `user_agent`); flags "does not handle new account creation" |
144
+ | 12 | [Saeloun — "Rails 8 adds built in authentication" (2025-05-12)](https://blog.saeloun.com/2025/05/12/rails-8-adds-built-in-authentication-generator/) | Enumerates everything left to build (sign-up, social, magic links, confirmation, locking) |
145
+ | 13 | [Avo — "Rails 8 Authentication with the auth generator" (2025-01-27)](https://avohq.io/blog/rails-8-authentication) | Full file-by-file tour; "meant to kickstart our app's authentication and leave us with a solid foundation" |
146
+ | 14 | [Money Forward Dev — "Rails v8 new authentication generator" (2024-12-10)](https://global.moneyforward-dev.jp/2024/12/10/rails-v8-new-authentication-generator/) | Enterprise (Japan) engineering-blog validation |
147
+ | 15 | [Jeremy Kreutzbender — "Controller Tests with RSpec and Rails 8 Authentication"](https://jeremykreutzbender.com/blog/controller-tests-with-rspec-and-rails-8-authentication) | Filling the missing-RSpec-support gap |
148
+ | 16 | [RubyStackNews — "Rails 8 Authentication: Why the New Built-in Generator Matters (and What It Means for Devise)" (2026-02-16)](https://rubystacknews.com/2026/02/16/rails-8-authentication-why-the-new-built-in-generator-matters-and-what-it-means-for-devise/) | 2026 retrospective on the Devise-vs-built-in question |
149
+ | 17 | [WorkOS — "Building authentication in Rails web applications: The complete guide for 2026"](https://workos.com/blog/rails-authentication-guide-2026) | Vendor-neutral 2026 state-of-auth; explicitly covers listing active sessions across devices and "sign out others" |
150
+
151
+ **Aggregator threads** (titles + IDs verified via search 2026-06-10; comment content UNVERIFIED — HN returned 429 on fetch):
152
+ - ["Rails 8 Authentication Generator"](https://news.ycombinator.com/item?id=41922905) (~2024-10)
153
+ - ["Social login with the Rails 8 auth generator"](https://news.ycombinator.com/item?id=43239888) (~2025-03) — thread title itself signals the OAuth gap
154
+ - ["Rails 8 adds built in authentication generator"](https://news.ycombinator.com/item?id=43962701) (~2025-05) — the topic resurfacing 6 months post-release
155
+ - Reddit r/rails: no single canonical "Rails 8 auth vs Devise" thread surfaced via search (2026-06-10) — **UNVERIFIED/absent**; sentiment instead distributed across the tutorial posts above.
156
+
157
+ ---
158
+
159
+ ## Adoption data 2026
160
+
161
+ **Surveys.**
162
+ - Planet Argon **2024 Ruby on Rails Community Survey** (2,700+ devs, 106 countries): no dedicated "which auth solution" question (verified by full review of [railsdeveloper.com/survey/2024](https://railsdeveloper.com/survey/2024/), fetched 2026-06-10), but **Devise ranked #1 in both "Which Ruby gems do you love?" and "Which Ruby gems frustrate you the most?"** — the love/hate profile that creates switching energy. Highlights also covered by [Socket.dev](https://socket.dev/blog/highlights-from-the-2024-rails-community-survey).
163
+ - **2026 survey**: open through 2026-07-03, "deeper" questions incl. AI workflows; results to be published free after close ([railsdeveloper.com/survey](https://railsdeveloper.com/survey/); [Planet Argon blog, 2026-04](https://blog.planetargon.com/blog/entries/the-2026-ruby-on-rails-community-survey-is-open); [Robby on Rails, 2026-04-27](https://robbyonrails.com/articles/2026/04/27/less-opinions-more-data-the-2026-rails-survey/)). **Auth-share numbers for 2026: not yet available** — re-check after July 2026.
164
+
165
+ **Gem telemetry** (RubyGems.org API + BestGems API, all fetched 2026-06-10/11):
166
+
167
+ | Gem | Total downloads | Latest version (date) | Read |
168
+ |---|---|---|---|
169
+ | devise | **280,870,110** | **5.0.4 (2026-05-08)** | Alive and on a 5.x major; requires railties ≥ 7.0 |
170
+ | sorcery | 7,270,473 | 0.18.0 (2025-12-06) | Niche, maintained |
171
+ | **authtrail** | **4,114,257** | **1.0.0 (2026-04-04)** | "Track Devise login activity" (ankane) — login-activity demand proxy |
172
+ | clearance | 2,158,451 | 2.12.0 (2026-04-17) | thoughtbot's minimal auth, maintained |
173
+ | rodauth | 890,858 | 2.44.0 (**2026-06-08**) | Jeremy Evans, extremely active |
174
+ | authentication-zero | 426,848 | 4.0.3 (**2024-10-26 — stalled**) | Pre-Rails-8 generator gem, obsoleted by the official one |
175
+
176
+ **Devise daily-download trend** (BestGems API [bestgems.org/api/v1/gems/devise/daily_downloads.json](https://bestgems.org/api/v1/gems/devise/daily_downloads.json), fetched 2026-06-10), weekday samples:
177
+
178
+ | Window | Typical weekday range | Peak sampled |
179
+ |---|---|---|
180
+ | Early Jun 2024 | ~98k–192k/day | 192,172 (2024-06-06) |
181
+ | Early Nov 2024 (Rails 8 launch week) | ~91k–162k/day | 162,313 (2024-11-06) |
182
+ | Early Jun 2025 | ~95k–180k/day | 179,537 (2025-06-04) |
183
+ | Early Jun 2026 | ~141k–239k/day | 238,811 (2026-06-10) |
184
+
185
+ **Interpretation:** Devise installs *grew* ~20–25% from mid-2024 to mid-2026. Downloads track CI/deploys of the installed base, not new-app choices — so the honest 2026 model is: **new apps increasingly start on the Rails 8 generator** (every official tutorial since 2024-12-13 does — [rubyonrails.org](https://rubyonrails.org/2024/12/13/learn-Rails-8-tutorial-and-unpacked-videos)), while **the Devise installed base keeps compounding**. A sessions/devices gem must serve both. No major app's public "we migrated to Rails 8 auth" case study was found beyond Radan Skorić's guest write-up ([radanskoric.com](https://radanskoric.com/guest-articles/from-devise-to-rails-auth)) — **claim of major-app migrations: UNVERIFIED/none found**.
186
+
187
+ ---
188
+
189
+ ## Evidence of demand for session tracking
190
+
191
+ The pattern: Rails 8 writes `ip_address`/`user_agent` to a `sessions` table and stops; Devise's `trackable` stores only the *latest* sign-in. Anyone who wants a GitHub/Google-style "Your devices" page builds it by hand — repeatedly:
192
+
193
+ 1. **WorkOS 2026 Rails auth guide** ([workos.com/blog/rails-authentication-guide-2026](https://workos.com/blog/rails-authentication-guide-2026)) frames it as a checklist item: "If you need to see all active sessions across devices ('You're logged in on iPhone, MacBook, and Windows PC – sign out others?'), the database makes this straightforward." Straightforward — yet not shipped by anything in the default stack.
194
+ 2. **SupeRails / Yaroslav Shmarov — "Manage active sessions in Rails"** (2024-03-24, [blog.superails.com/secutiry-manage-active-sessions](https://blog.superails.com/secutiry-manage-active-sessions)): "to enhance security of your application you will want to allow users to see all the devices/browsers they are logged in with" — full hand-rolled build (list + revoke).
195
+ 3. **SupeRails — "Devise has_many :sessions — track, list, and revoke active sessions"** ([blog.superails.com/devise-multiple-sessions-warden-hooks](https://blog.superails.com/devise-multiple-sessions-warden-hooks)): Warden-hook approach, per-browser UUID in the encrypted cookie mapped to a DB Session row; "when a session is revoked, the next request from that browser forces a sign-out." Three files + migration of bespoke plumbing — exactly what a gem should absorb.
196
+ 4. **"Setting up multi-device/browser session tracking for Devise"** ([rails.substack.com](https://rails.substack.com/p/setting-up-multi-devicebrowser-session), syndicated on [RubyFlow](https://rubyflow.com/p/95puif-setting-up-multi-devicebrowser-session-tracking-for-devise)): a `LoginSession` model with IP, status (`active`/`inactive`/`locked_out`), session ID — "display them in 'your active sessions' UI, and let users revoke sessions remotely."
197
+ 5. **authtrail at 4.11M downloads, 1.0.0 on 2026-04-04** ([rubygems.org/gems/authtrail](https://rubygems.org/gems/authtrail), API fetch 2026-06-10): the market already pays (in installs) for *login activity tracking* — but authtrail is Devise-only and logs attempts; it doesn't manage live sessions/devices.
198
+ 6. **Andrii Furmanets' comparison** ([andriifurmanets.com](https://andriifurmanets.com/blogs/built-in-authentication-in-rails)): the Rails 8 generator "keeps track of sessions history instead of just storing the latest session information like the trackable module does. This allows for a more granular control of user sessions" — the data advantage is recognized; the missing layer is product surface.
199
+ 7. **GoRails impersonation episode** ([gorails.com](https://gorails.com/episodes/how-to-add-impersonation-to-rails-authentication-generator)) demonstrates the generated `Session` row as the natural place to hang per-session state — the same extension mechanism `sessions` uses.
200
+ 8. **Rails security guide** ([guides.rubyonrails.org/security.html](https://guides.rubyonrails.org/security.html)): session hijacking is a first-class documented threat ("Stealing a user's session ID lets an attacker use the web application in the victim's name") — yet the framework offers no user-facing mitigation surface (view/revoke sessions). UNVERIFIED quantitative SO data: no Stack Overflow view-count evidence collected for "devise list active sessions" queries (search surfaced blogs, not SO threads; 2026-06-10).
201
+
202
+ ---
203
+
204
+ ## Implications for the sessions gem
205
+
206
+ 1. **The substrate is now standard.** Since 2024-11-07 every new Rails app can have a DB-backed `Session` row with `ip_address`/`user_agent` for free. The gem doesn't need to argue for database sessions — DHH already did ([rubyonrails.org, 2024-11-07](https://rubyonrails.org/2024/11/7/rails-8-no-paas-required)). We are the missing presentation/management layer on data Rails already collects.
207
+ 2. **The exclusions are policy, not backlog.** DHH's "not an all-singing, all-dancing answer" + "that's not going to happen with this generator" ([PR #52328](https://github.com/rails/rails/pull/52328)) and the 8.1/8.2 release notes (zero auth additions) mean Rails core is *not* going to ship a devices page, "sign out everywhere", or login notifications. Low platform risk through at least Rails 8.2.
208
+ 3. **Position with the doctrine, not against it.** The winning frame: "You own your auth (omakase + sharp knives); we add the session/device layer every real app needs — drop-in, readable, removable." A heavyweight auth framework would fight the 2023-12-26 thesis ([issue #50446](https://github.com/rails/rails/issues/50446)); a focused tracking/management gem rides it — like the community's own preference for "a gem pool of different auth generators" ([discuss thread, 2024-11-24](https://discuss.rubyonrails.org/t/proposal-authentication-via-magic-links/87944)).
209
+ 4. **Serve three installed bases**: (a) Rails 8 generator apps (fastest-growing, every official tutorial), (b) Devise's compounding ~280M-download base — still growing ~20-25% YoY in daily installs (BestGems, 2026-06-10) — where multi-session tracking requires hand-rolled Warden hooks today, and (c) OAuth/omniauth sign-ins, which the generator ignores entirely. First-class adapters for all three is the moat; SupeRails' two separate hand-rolled implementations (one per stack) prove the duplication pain.
210
+ 5. **Ship what tutorials keep re-teaching**: active-sessions page, device naming (parse `user_agent`), per-session revoke + "sign out all other devices", session history/login activity (authtrail's 4.1M downloads validate), suspicious-login signals (new device/IP). Each maps to a documented hand-build above (§7).
211
+ 6. **Timing**: 2026 surveys land in July 2026 ([railsdeveloper.com/survey](https://railsdeveloper.com/survey/)); Rails World Austin is 2026-09-23/24. Both are natural launch/content windows. Re-verify auth-share numbers when Planet Argon publishes.
212
+ 7. **Naming collision note**: DHH's PR was literally titled "Add basic **sessions** generator" ([#52328](https://github.com/rails/rails/pull/52328)) before becoming `generate authentication` — the word "sessions" is now firmly associated with DB-backed auth rows in Rails mindshare. Good for discoverability of a gem named `sessions`; docs must disambiguate from `ActionDispatch::Session` cookie store.
213
+
214
+ ---
215
+
216
+ *Methodology: web research 2026-06-10/11 (WebSearch + direct fetches of rubyonrails.org, guides.rubyonrails.org, github.com/rails/rails templates via raw.githubusercontent.com, RubyGems API, BestGems API). Failed fetches noted inline: thenewstack.io article body (page chrome only), news.ycombinator.com (HTTP 429), rubyevents.org (HTTP 403). Tweet dates inferred from snowflake IDs. All other quotes verified against the cited page on the fetch date.*