Supporting reference · Working design and evidence, not a promise of complete support.

On this page

Native capability for the Rails target

Status: Normal Compilation independently emits selected iPhone and Android projects under ios/ and android/. Each receives the Web main navigation: admitted public and protected indexes plus the Account entry. A realized Account signs in through its generated web forms inside the native shell and restores the session with Rodauth remember, as browsers do; Native Accounts describes the flow. Launcher icons derive from the application name. Push and the placeholder release identity of a Plan without a domain remain precise reviewed gaps. Tests and CI hold the implemented claims, and the evidence index records the Revyl, Simulator, and hosted observations.

Hotwire Native is the first Rails-specific approach because it renders the server's HTML inside ordinary iOS and Android applications, uses platform navigation, and permits selective Swift/Kotlin enhancement. See the official Hotwire Native overview and architecture explanation.

This Capability design is a current implementation direction, not the definition of owned native output. A controlled comparison may later favor another client architecture.

Why this is a Capability

The compilation target remains Rails. Native changes how that Rails application is presented, built, notified, and delivered; it does not replace the backend architecture.

The Plan requests each client independently by including an enabled-object fact at native.ios or native.android; an omitted platform requests no client. Native is therefore neither subject-triggered nor unconditional. Each fact can later gain bounded platform-specific configuration such as a bundle identifier without changing the selection shape. The Capability catalog (repository-only) lists ten subject triggers and none of them can imply a native client, so waiting for a trigger would mean never emitting one. Decision 0016 (repository-only) records why the switch is per platform: a Mac is required to build the iOS project but not the Android one, and the two distribution programs are separate memberships obtained by different people.

The complete Capability direction would emit:

The output may span several directories. That does not make native universal Core or a peer target profile.

Omitting a platform requests none of its project, build and test workflows, path configuration, or Compilation Record coverage. It does not change Web main navigation (repository-only), which derives independently from admitted public and protected Scaffold indexes plus one Account entry. Each selected iOS and Android client lowers that same navigation into native tabs. Account meaning never implies an Apple client or a tab bar.

Each platform lowers independently. A requested client without a usable destination or platform identity is omitted and listed in the reviewed GapSet while a coherent Rails application can still compile. Requesting both platforms does not require both to be lowerable.

Native theme policy

Omitting application.appearance.theme selects fixed light. Explicit light and dark select that appearance in the native shell and embedded Rails pages. auto follows system appearance. Both native color palettes remain available for these modes.

The authored toggle choice provides the browser's Light / Dark / System selector and browser-local preference. Selected native clients are still emitted, but use automatic appearance. Their embedded Rails responses also use automatic appearance and omit the browser selector and preference storage. A browser preference cannot by itself control platform window appearance; no native preference control or bridge is generated.

Analysis records this residual once at /application/appearance/theme, with code foundation_plan.gap.appearance.native_theme_preference.not_generated and status partially_generated. The record names only the emitted iOS and Android clients. A browser-only Plan or an omitted native client contributes no such gap. Compilation carries the same reviewed GapSet and preserves the authored toggle in the retained Plan. The native mappings are covered by emitted-source checks; native runtime switching is not qualified by browser theme tests.

Placeholder release identity

Many owners have no domain when they start, so a Plan may omit application.domain. A selected client is still emitted. Without a domain, its Rails origin ends in the reserved .invalid top-level name, which never resolves, and its bundle or application ID starts with invalid.firstdraft. The platform sections below give the exact forms. These placeholders cannot reach another party's server or take a real domain owner's reverse-DNS identifier.

The placeholders also mean the Release build cannot reach the application's server and cannot be submitted to a store. Analysis therefore records one foundation_plan.gap.native_client.partially_generated entry with status partially_generated at /application/native/ios or /application/native/android for each emitted client. The entry names the placeholder origin and ID. It tells the owner to set application.domain, or replace both values, before a Release build, because a store bundle or application ID cannot change after the app is published. An authored domain, or a client that is not emitted, adds no entry.

The owner chose this disclosure during the second app-quality audit (repository-only). Selecting a client is authored meaning, and a client that cannot be released delivers that request only in part. The GapSet is the record the authoring agent and the Skill's read-back review most closely, so the limitation belongs there rather than in an emitted constant.

Android application lowering and runtime

AndroidApplication consumes the same target-neutral application navigation as Web and iOS, including protected indexes and Account self. The versioned foundation-plan-rails/android-application-2026-09 lowering maps all 28 semantic icons to pinned Material assets. A domain supplies the HTTPS Rails origin and reversed application-ID prefix. Hyphens become _h; a numeric-leading label receives d_. DNS labels contain no underscores, so these transforms preserve distinct identities. The application key remains the final Java package component. Android IDs are capped at the platform's 223-byte package-name limit. Without a domain, the identity uses the placeholders invalid.firstdraft.<application_key> and https://<application-key-with-hyphens>.invalid.

AndroidApplicationTaskPlan retains one digest-verified Core archive and its replacement seams: application properties, generated Kotlin definition and tests, the activity layout, strings, light and dark colors, the launcher foreground, and bundled path configuration. AndroidRenderingContext binds the exact captured input, its Android and Appearance projections, the generated form routes, and the unchanged submitted Plan digest. The composer rechecks seams, owners, modes, and provenance before mounting android/. Unselected or declined Android never loads its Core archive. The APK's manifest and BuildConfig carry the Plan digest, and generated tests compare it with the Kotlin definition. Application IDs are separate from the stable Kotlin namespace so an app-specific identifier needs no source rewrite.

Core uses Hotwire Native Android 1.3.1, Material DayNight views, JDK 17, Kotlin 2.3.0, AGP 8.13.2, and the checksummed Gradle 9.2.0 wrapper. The initial build targets Android 16/API 36 with minimum API 28. That minimum is a build configuration, not evidence that every supported OS/device has been exercised. Native libraries, Google services, Firebase, signing credentials, and Play publication are unnecessary for the generated debug preview.

One destination uses a single Navigator without a tab bar. Two through five use Hotwire's tab controller and separate back stacks. More than five use four tabs plus a native More list containing every remaining destination. Overflow routes push from More so Back returns to that list; only direct roots replace their tab's root. Every generated new and edit route, gated or public, uses a full-screen Hotwire Web destination with context: modal and pull-to-refresh disabled, matching the pinned Android demo. Rails owns HTML, forms, validation, and server redirects; Hotwire owns visits, cookies, and history. The Compiler bundles the rules, and the generated Rails app serves the same file as Hotwire's remote source; Path configuration describes both.

Tab labels remain visible, wrap, and follow system font size. Authored Entity icons supply meaningful tab symbols; an absent icon retains the semantic grid fallback. Core handles phone rotation in the existing activity, following the pinned Hotwire demo. The optional bottom-sheet destination remains registered, but generated forms use the full-screen modal because the nested sheet failed to scroll after validation increased its content height. Unsaved draft restoration after process death or other activity recreation is separate. Failed regular and modal page loads expose a Try again button through Hotwire's public refresh method; no write is queued or automatically retried.

Path configuration selects destinations and modal presentation; it does not remove HTML inside the WebView. Rails Core uses hotwire_native_app? to omit the web header and persisted browser theme for native requests, including new/edit forms. The footer's /privacy and /terms links remain. This applies with or without authored Appearance. Native HTML supplies a concise page title, falling back to the app name when no title is set. Generated public Scaffold pages hide repeated app branding and keep their page heading available to screen readers without displaying it again below the native toolbar. Browser requests keep their header and visible page heading. Native buttons, inputs, selects, and table links have a three-rem minimum target height; buttons and table links also have a three-rem minimum width. Ordinary browser sizing remains unchanged. Browsers show the explicit collection destination; native requests omit that link and rely on client navigation. Omitted theme selects light; auto and the native toggle residual follow the WebView's reported color preference.

Material themes apply authored automatic/light/dark appearance, accent, and background colors. The native root owns side cutout/system insets and keyboard space, Hotwire owns the top toolbar inset, and visible Material tabs own the bottom system inset. The root consumes the insets it handles. A single destination reserves bottom system space itself; a full-screen modal reserves any remaining bottom inset because Hotwire hides its tab bar. Material's OnSurface toolbar style keeps close/back icons visible in both light and dark mode. Core assigns Back and Close descriptions through Android string resources after Hotwire installs the toolbar icon. The labels preserve Hotwire's navigation behavior; accessibility hierarchy checks do not establish TalkBack speech or focus order. Push remains ungenerated.

Debug accepts APP_ROOT_URL as an Intent string extra, including Android Emulator loopback 10.0.2.2. Release ignores it and uses the generated HTTPS origin. The Codespaces warning workaround uses Hotwire's public custom-WebView hook and Android's loadUrl header overload, scoped to the exact configured public Codespaces origin. It does not bypass private-port authentication. The Core's upstream comparison (repository-only) records why no second networking or navigation library is needed and when to retire this small hook.

Initial iOS application lowering

The immutable foundation-plan-rails/ios-application-2026-08 release owns the first pure iOS transforms used by Compiler composition. It maps all 28 semantic icon tokens to normal and selected SF Symbol names. It also translates underscores in application.key to hyphens and requires the resulting component to remain one DNS-safe label of at most 63 ASCII bytes.

A present domain must remain at most 253 ASCII bytes with no label over 63 bytes, must have a final label beginning with a letter, and cannot use the Compiler-owned .invalid placeholder namespace. The lowering uses it for the Rails HTTPS origin and reverses its labels for the bundle prefix. Without a domain, it deliberately produces the placeholders https://<application-key-component>.invalid and invalid.firstdraft.<application-key-component>. The same release admits one-line display names and encodes display names and closed HTTPS roots so Xcode preserves literal dollar signs, double slashes, quotes, Unicode, and edge whitespace. Its xcconfig encoder is nested inside the versioned lowering boundary; changing those bytes requires a successor release and a fresh Xcode audit. Display-name character policy remains Plan-wide; this release does not add a separate iOS-only Unicode policy. The release pins the SF Symbol-name bytes, while runtime resolution at the Core's minimum deployment target remains unproved until the generated-app Simulator smoke.

The SQL-free IosApplication projection consumes one sealed Project generation and its same-generation ApplicationNavigation result, the same value Web navigation uses. It retains the Rails route-collision diagnostics and source identities needed by Web and native consumers. A declined iOS client carries no application output, navigation entries, diagnostics, task, Core package, or native provenance output. A selected client lowers the application identity, origin, Xcode scalars, and every qualified navigation icon. It retains valid entries beside application-navigation or iOS-specific diagnostics, so a successful result can navigate only to exact Rails routes admitted for Web emission.

Selected iOS reports source-addressed diagnostics when the application key or domain cannot form the pinned iOS identity, when the display name cannot fit one admitted Xcode configuration line, or when no valid navigation entry remains after an otherwise successful application-navigation projection. An upstream failure already explains why no navigation entry survived, so iOS does not restate that failure as a missing-navigation diagnostic. A missing domain is not an error. The projection marks the resulting .invalid origin and invalid.firstdraft bundle prefix as a placeholder identity, which the reviewed GapSet discloses.

An inadmissible key or domain leaves the selected result without an application value because no identity can be emitted. An inadmissible display name retains the independently lowered identity and origin but leaves the encoded Xcode display name absent. Consumers proceed only from a selected, successful result.

iOS application renderer

IosRenderingContext binds the exact qualified CompilationInput::Result, its exact successful iOS projection, its exact ApplicationAppearance projection, the generated form routes, and the lowercase SHA-256 of the exact Plan snapshot. CompileCapturedProject serializes that captured Project once, and the active ApplicationManifest coordinator forwards its digest unchanged. The renderer does not reconstruct provenance from either smaller projection or accept a reconstructed same-generation value.

Selected iPhone and Android clients cause the complete application README renderer to include their preview-guide links and Rails startup command. The guides retain platform build prerequisites and preview limits. README keeps the private Codespaces port default and routes temporary remote-preview exceptions and cleanup to the selected guide. This is documentation routing, not device or vendor-tool proof. The Core composition owner describes application-document ownership.

The explicit renderer:ios_application family selects no tasks for a declined client and one one-path task per declared Core replacement for a selected client. Those paths emit application identity and build settings, navigation definitions, a bundled path configuration, generated unit and UI tests, the AccentColor and FoundationBackground color-set definitions, and the AppIcon image. The generated definition retains the Project ID, graph version, target, profile, lowering release, Plan digest, and .automatic, .light, or .dark Appearance theme. Entity-derived files carry the stable UUIDs of their navigation sources. The former aggregate call remains only as a byte-for-byte differential test oracle.

Scalar authored colors expand to both modes; pairs become exact sRGB light and dark asset entries. Each omitted color uses its neutral Core value independently: an empty named accent for missing tint and white/black Foundation backgrounds for missing background. Absent and theme-only Appearance therefore emit both neutral assets. Omitted theme selects light and emits INFOPLIST_KEY_UIUserInterfaceStyle; auto and the native toggle residual omit that value and emit .automatic, so iOS follows the system. Core owns the launch storyboard's FoundationBackground reference and applies the theme, tint, and background to the window, Hotwire destination controllers, custom Web views, their scroll views, and their under-page color. The generated client does not duplicate those runtime surfaces.

The bundled configuration begins with Core's default push and pull-to-refresh behavior. One later, query-safe rule for each direct tab's root sets presentation: replace_root. With up to five entries every ios_navigation root is a direct tab, including /account; with more, only the first four are. A realized Account also adds the session-change rule that Native Accounts describes. The renderer then adds one rule for each GET form route that the Web Scaffold renderer writes into config/routes.rb: each new and edit resource route, each associated-create new route below its parent, and /account/edit when the Account is editable, whether an Account or Policy gates it or not. Each parameter segment matches one path segment, and an optional query string follows; the rule sets context: modal and pull_to_refresh_enabled: false. Refresh remains enabled for ordinary pages; form drafts do not acquire a pull-to-refresh action. Other links retain Core's default push behavior. The renderer does not infer modal routes from navigation strings.

One navigation entry generates tests for Core's ordinary navigation stack. Two through five entries generate tests for every direct tab and label. With six or more entries, the first four entries remain direct tabs and the fifth tab is Core's native More list, as on Android. The generated test opens each listed destination and returns with Back. Every tab loads on first selection, so a cold launch makes one page request. Core does not use UIKit's More controller, which opens only navigators that have already started: every remaining destination would load at launch, in parallel and without a Rails session cookie, and each would create its own CSRF token. A form among them, such as the Account page's Sign out, would then fail its CSRF check (upstream discussion).

A selected client with no entry cannot render. Before any task renders, IosApplicationTaskPlan retains one exact IosCorePackage, requires every declared seam with its expected owner and mode, and rejects missing or undeclared seams. The normal sequential executor then gives a rendering exception or SQL violation the exact file-task identity. The tasks are independent and need no DAG. IosCoreComposer receives that same retained package and repeats the seam, owner, and mode checks before composition. Focused Ruby proof composes the manifest with the pinned Core. It requires the rendered xcconfig settings, path settings, Swift members, asset catalogs, generated-test contracts, and navigation initializer labels that the pinned fixture and immutable Core source consume, then removes every standalone fixture marker. The renderer is selected only when the exact qualified input selects iOS; a browser-only application has no iOS tasks and never loads the iOS package. Deriving release versions and build numbers remains deferred. No composed generated application has run through Xcode or a Simulator as part of hosted or Compilation-lifecycle automation. An opt-in repository harness now repeats one five-Entity generated Debug application against generated Rails through native tabs. A dated field report (repository-only) records one staff-prepared generated application passing its iOS checks and displaying generated Rails pages in an iPhone 17 Pro Simulator.

A second opt-in harness exercises the Movie Catalog Account shape on a copied, instrumented iOS tree. Rails authentication names it.

Core owns a per-launch APP_ROOT_URL preview seam (repository-only). Debug accepts an HTTPS host root or HTTP loopback root from the launch environment or -APP_ROOT_URL argument; Release ignores both and uses the compiled HTTPS origin. The dated Simulator observation used that seam rather than exercising the emitted production origin.

Core continues to own the window, navigation and tab controllers, transparent scroll-edge appearance, and the safe-area-aware launch storyboard. It also owns Appearance application from the named launch color through runtime window and Web-view backgrounds. Generated Swift and color assets supply data to those seams; they do not draw another header, tab bar, safe-area inset, or Dynamic Island treatment.

iOS Core pin and composition

An adjacent implemented source boundary bundles the current iOS Core pin (repository-only). Its deterministic archive excludes the entire .github directory, which currently contains one hosted workflow that would be inert inside generated source. IosCorePackage verifies the archive through the same byte, revision, path, mode, and collision policy as Rails Core. IosCoreComposer can mount the source under ios/, preserves Core ownership and modes, and writes ios/FOUNDATION_PROVENANCE.json without changing artifact-envelope format v1. That file records the source, archive root and exclusion, mounted path, replaced paths, and generated additions so it describes both the archive recipe and the deliberately composed tree. The composer treats every file under a Core Generated directory as a declared seam, rejects an undeclared new seam, and requires every current application identity, navigation, route-configuration, application-test, accent-color, background-color, and AppIcon path from its exact renderer owner with its Core mode. A pinned whole-output test also rejects the current fixture's operational identity and generated test markers after composition. The packaged application requires FoundationApp/Generated/ios_v1.json and passes it to Hotwire Native before the server URL. The pinned Hotwire Native 1.3.0 loader applies sources in that order: it loads the bundled file synchronously, then a cached remote value when present, and starts the remote request asynchronously. A failed or non-200 request returns without replacing the current configuration: the bundled rules on first launch, and the last fetched copy after that. The generated Rails app answers that request, as Path configuration describes.

The pinned Core now advertises an iPhone-only device family. Its Simulator tests exercise the real single-navigator and native-tab containers against a Core-owned sentinel document. UIKit and Hotwire Native own the native safe-area adjustment, so composed Rails output must retain ordinary viewport behavior instead of adding generic viewport-fit=cover plus env(safe-area-inset-*) padding. Those tests constrain Core; a composed Rails smoke remains necessary. iPad qualification is deliberately deferred in iOS Core (repository-only) until the navigation and safe-layout contract is designed and exercised there.

IosApplication consumes the exact same-generation iOS navigation result. It maps semantic icon tokens to the pinned SF Symbols only after iOS selection. Account self may be the sole entry, which produces a single Navigator. Its entries keep the target-neutral stable identity, route, label, rank, and source provenance. The task family reads the sealed Project only to prove the exact context generation and performs no SQL. The independent package pin belongs to the Compiler's provisional native Capability rather than the Rails naming profile. Release-version and build-number derivation remain later work.

Appearance is consumed by Rails and selected native shells. iPhone and Android lower independently; delivery is pruned at import. With no iOS selection, browser application navigation remains Web input while iOS declines without output.

Launcher icons

Selected iPhone and Android clients reuse the monogram that the application name selects for the Web bookmark icons. The Plan owner describes that selection and its neutral fallback. The artwork is black on white whatever Appearance requests, so the reviewed GapSet carries no icon record. Custom icon fonts and colors remain deferred (repository-only).

script/build_bookmark_icons.mjs draws every monogram once, and the Compiler copies the finished files, so Compilation loads no font or rasterizer. For iPhone, the script renders the 512-unit bookmark artwork at 1024px and removes the alpha channel, because App Store validation rejects a transparent icon. That image replaces iOS Core's AppIcon.png. Core's AppIcon Contents.json names the image, so Xcode records CFBundleIconName when it compiles the asset catalog. iPhone receives no separate dark or tinted icon variant.

For Android, the script centers the glyph in a 48dp box inside the 108dp adaptive-icon layer. Launchers show the inner 72dp, and Android asks for a logo of at least 48dp. Android's safe zone is the centered circle 66dp across. A 48dp A, V, or X would put its corners outside it, so those glyphs shrink slightly to fit. That vector replaces Android Core's ic_launcher_foreground.xml. Core draws it on a white background in light and dark mode and reuses it as the monochrome layer, which Android 13 and later tint for themed icons. Each Core's standalone fixture keeps a geometric F.

iOS forms and readable content

The first controller in a modal stack supplies UIKit's localized Cancel control when no custom left item exists. Cancel dismisses; the Rails form's Create or Update button saves. A pushed modal controller keeps Back. Core replaces Hotwire's Done item because its checkmark looked like a save action while only dismissing the form. This adds no draft tracking or discard-confirmation bridge. The Rails form also retains its Cancel link to the explicit collection or return URL. That destination may differ from dismissing the native modal stack, so both controls remain; the native item selects no Rails destination.

UIKit and Hotwire retain ownership of physical-screen, navigation-bar, tab-bar, and home-indicator insets. Core anchors Hotwire's public visitableView below the navigation bar with view.safeAreaLayoutGuide.topAnchor; its bottom remains at view.bottomAnchor, with automatic scroll insets retained. The WebView scroll view uses keyboardDismissMode = .onDrag. The local landscape trial kept the focused Rails field below the toolbar, then dismissed the software keyboard by dragging from that field and exposed the complete Create action. No CSS safe-area padding, keyboard-height calculation, or JavaScript keyboard bridge participates.

Rails Core uses WebKit's supported font: -apple-system-body on the native root so Dynamic Type scales rem text, controls, and spacing together. The observed Simulator root was 17px at default size and 53px at the largest accessibility size. The rule requires the native body marker and both system-font and :has() support; otherwise existing root styling remains. Ordinary browser and Android roots retain their sizing. The CSS cascade preserves configured font family and line spacing; application-authored fixed pixel sizes do not inherit the scale.

Native buttons wrap within their container and grow vertically while retaining their 3rem minimum and larger component sizes. Basecoat and React controls share the native sizing policy. Generated Rails submit actions use form.button, because the Simulator trial's single-line submit inputs could not wrap the full Create Location label. Browser buttons retain the selected Vega component sizing. The iOS interaction report (repository-only) separates temporary probes from passing ordinary-Compilation runs at default and largest accessibility text sizes. These observations do not establish physical-device, older-iOS, or general accessibility conformance.

Browser preview with Revyl

Revyl is the primary native preview for iPhone and Android, in a Codespace and on a local computer, by the 2026-09-30 decision (repository-only). Locally, a cloudflared quick tunnel gives Rails a public HTTPS address. Xcode's Simulator and Android Studio's Emulator are documented alternatives. The emitted preview guides follow this order. The packaged Skill and the local guide still present the earlier order. Leave the local guide to the tester guide now in progress.

The local guide covers local Rails, local native builds, and direct Revyl uploads. The evidence index records which Revyl sessions loaded each client through Cloudflare Tunnel and which Android WebView versions they reported. Hotwire's minimum is WebView 120.

Android Studio's local Emulator with a compatible WebView is the Android alternative, and the fallback when a hosted device reports a WebView older than 120. Rails can also stay in a Codespace behind a private GitHub CLI port-forward; the Emulator reaches that local port through 10.0.2.2. The generated Android guide keeps a short optional Emulator section with Debug launch flags. The private-tunnel observation (repository-only) proved the connection with an existing APK and Rails app. The later assisted Codex trial (repository-only) exercised integrated-terminal bin/dev, a fresh clone opened in newly installed Android Studio, a local build and Emulator interactions, and Rails-only refresh without rebuilding the APK. Studio's bundled Java 25 worked; forcing its Gradle JVM criteria to 17 failed. The guide retains Studio's default and separately documents JDK 17 for CLI/CI. Core's bytecode target remains Java 17. The local Emulator does not require Revyl build or device usage.

Selected output also includes Revyl preview instructions: IOS_PREVIEW.md (repository-only) and bin/ios preview revyl for iPhone, or ANDROID_PREVIEW.md (repository-only) and bin/android preview revyl for Android. Each guide offers two ways to give Rails a public HTTPS address: a local cloudflared quick tunnel with Rails started as RAILS_DEVELOPMENT_HOSTS=.trycloudflare.com bin/dev, or a public Codespace port. Rails reads that variable at boot, Core also applies it from .env.development.local, and the leading dot admits every quick-tunnel host. When the preview helper's /up check gets HTTP 403, its error names that command. The guides also say to run one Revyl device at a time and to sign in to Revyl in the browser with the CLI's account before opening the Viewer link. GitHub builds an unsigned Debug Simulator artifact on a Mac runner for iPhone and a standalone debuggable APK on an Ubuntu runner for Android when native source is pushed, so the Revyl path needs no local Xcode, JDK, or Android SDK. Rails-only commits reuse an ancestor's artifact when all native build inputs match. Revyl runs the uploaded artifact against the public Rails address through APP_ROOT_URL. Students keep most Rails iteration in their web preview and stop Revyl after native checks. GitHub runner allowance and Revyl device usage are separate; this path uses no Revyl remote-build compute.

Install the latest Revyl CLI with its official installer. The preview helper checks authentication and the app API before requesting a build; doctor reports the installed CLI version without enforcing exact equality. The owner accepts ordinary CLI drift: a future command change can be adapted in the generated application. A version bound needs an observed incompatibility, rather than an untested release alone. Commands and JSON shapes were checked against v0.1.129 source without a new provider session.

The Codespaces token can push and download artifacts while denying workflow dispatch. The guide therefore retains an Actions-page fallback for an expired artifact. Debug WebKit loads to the configured Codespaces origin skip its non-Turbo warning page with the documented tunnel header. The port must still be public, and Release behavior does not change. The scoped request decoration lives in Core's AppWebView (repository-only). Preview configuration holds no credentials and does not require a Revyl GitHub integration. The unsigned artifact carries no Apple signing identity, and Decision 0008 (repository-only) keeps Revyl a replaceable adapter over it. Physical iPhone installation is a separate path.

The September 10 smoke (repository-only) preserves provider repairs and the ecosystem comparison. Revyl previews have shown fresh deployed output and sign-in; reliable restart and browser drag and Back remain unshown.

Emitted IOS_PREVIEW.md still teaches pull-down refresh and waiting before a restart. The September 12 smoke at 651d8d62 showed why those instructions need supplements: browser drags did not refresh, while this CLI swipe refreshed the default portrait preview:

revyl device swipe down --x 210 --y 310 --duration 1200

Its wait-for-completion advice also did not resolve a restart rejection: Revyl reported its concurrency limit after the first session was Completed and the dashboard showed zero active devices. If that happens, stop and record the error rather than repeatedly retrying or upgrading to continue. The cause remains unresolved. These corrections supplement the emitted iOS guide. The Android Studio follow-up changes no iOS template or runtime and adds no new Revyl observation.

Native does not own which surfaces are top-level. The implemented main navigation (repository-only) derives browser navigation from admitted public, item-authorized, and environment-gated Scaffold entries plus one Account entry. Any realized Account contributes account-self:<Account subject_uuid>, the semantic person token, and /account whether or not iOS is selected. The browser shows Account after resource entries and before Sign out when current_account is present. Authored profile details compose into that destination under their own read Policy. Selected iOS and Android clients consume that same navigation, protected entries and Account self included, and place Account last as the browser does. An omitted client emits no native output.

Both clients open on their first tab. The home_index (repository-only) Entity's index is therefore the first entry, and the other indexes follow Entity order, so the app opens on the page the Web root serves. Without a Home choice, or when navigation omits that index, the first Entity's index opens first. With more than five entries, Home therefore stays a direct tab on both clients rather than moving into More.

The lowering needs two things the derived list does not carry.

An icon per entry. Each index takes its glyph from the optional icon on the Entity behind it, and the Account self entry takes its fixed person token. Icons are semantic tokens rather than platform names, in the same way Field types are semantic tokens rather than Ruby classes. A separately versioned platform lowering maps each token to an SF Symbol on iOS or a Material icon on Android.

That token vocabulary has to be a closed set. An open string would let an author write a name that resolves on one platform and silently falls back on the other, which is the kind of per-platform divergence this Capability exists to prevent. Curating the mapping is real platform-lowering work that no icon design avoids. Shared main navigation turns an omitted Entity icon into the neutral grid token, and the versioned iOS lowering maps that token to square.grid.2x2 and its selected variant.

A badge, with its count left to ordinary source. The Compiler emits the badge machinery and every entry can carry one; the number comes from application code at render time rather than from the Plan.

Ruby Native draws the same boundary. Its configuration carries only a per-tab boolean, while the count arrives through a native_badge_tag(count, home:, tab:) view helper. The value that changes per request stayed out of the static config, and the same reasoning applies here.

Binding a badge to a Predicate was considered. It is expressible today, since a Predicate can compare against { "kind": "environment", "name": "current_account" } as Shinar already does. Shinar's unread messages and Dunbar150's notifications are both natural badges. It was declined for now because a live COUNT on every render sits badly with the profile's strict_loading and flat-query-count discipline, so a declarative badge would quietly imply a counter or derived Field as well. Revisit once the causal seam (repository-only) closes and an inbox is a Plan subject, which would make the binding express real meaning rather than a query shortcut.

An application with one navigation entry has no tab bar. The client opens that entry directly, which matches Ruby Native's behavior when its tabs key is omitted. A selected iOS client with no entry is invalid even though an empty main-navigation result remains valid for the web boundary.

Native Accounts

Authentication stays in Rails, as Joe Masilotti recommends for Hotwire Native apps (10 tips, tip 3; authentication deep dive). The generated Rodauth login, registration, and recovery pages render inside the native web view. The session cookie is shared with Hotwire's requests, and Rodauth's remember feature restores the session after the app restarts. Native code holds no credentials and draws no sign-in screen.

Every realized Account emits Rodauth remember with a *_remember_keys table, for browsers and native clients alike. after_login remembers every login, and an autologin account creation is remembered too. load_memory runs on every Rodauth middleware request, and before_logout revokes the key. The deadline is a year out, and using the app moves it a year out again, so people who keep using the app stay signed in, as Joe Masilotti recommends (signed in forever, tip 5). The Rails authentication page owns that configuration.

When an Account is realized and iOS or Android is selected, the Compiler also emits:

A protected tab therefore shows the web sign-in while signed out. Rails redirects a guest to /login both from before_action :authenticate and from a Policy denial, while a signed-in Account without permission still sees the 403 page. After sign-in or a signup that signs in, every tab reloads signed in; a saved return-to location applies only to browsers. iOS returns to the first tab, the Home index when one is generated; Android keeps the selected tab.

Gated forms open as modals, so sign-in can start from one. In the iOS Simulator and an Android emulator, after the session expired on the server, tapping a New link that was still on screen led to sign-in on the tab's own stack with no modal open. Submitting a form already open in a modal after expiry fails Rails' CSRF check, which answers 422. With the production error pages, both clients showed "Change rejected" inside the modal, because Turbo 8.0.23 renders a 4xx form response in place when its tracked assets match the current page. Closing the modal returned to the tab: iOS kept the stale signed-in page, whose New link then led to sign-in, and Android reloaded the tab signed out, so sign-in started from Account. Development's debug page carries no tracked assets, so there Turbo reloads the form URL instead, which redirects to sign-in. In every case, signing in reached /native-session-changed, and the reset navigation left no modal on screen. /login has no path rule. Open question for the owner: whether a CSRF failure without a session should lead to sign-in, which would add a rescue to register G7 (repository-only).

Deferred: native sign-in screens, biometric unlock, a modal sign-in presentation, preserving the selected iOS tab across a reset, email-verification links inside the app, and push.

Path configuration

ios_v1.json and android_v1.json are derived output. The routes in config/routes.rb already say which URLs are single-record forms. Hotwire Native's demo configuration maps the bare new and numeric-edit patterns; the optional query-safe suffix is this Compiler's addition:

Derived from Emitted rule
Each generated new or edit route, gated or not context: modal, no pull-to-refresh; optional query
A direct navigation entry's route presentation: replace_root at that root, with or without a query string
A realized Account's /native-session-changed reset_navigation: true, a Core-owned custom property
Every other path default push navigation

The native_ios and native_android probes in the support inventory (repository-only) check the emitted replace_root and default push rules in each client's path configuration. The native_modal_forms probe checks the modal rule on a form that an Account gates.

Pinned Hotwire Native 1.3.0 enables query-string matching by default and evaluates rules against path?query when a query is present. The emitted optional query suffix keeps pagination or filter URLs at a navigation root under replace_root, rather than adding a Back entry for another state of the same root index. It likewise keeps a generated form modal when its URL carries a query. Focused Compiler tests prove the emitted expressions; the current generated-app observation did not tap a query-bearing root or form URL.

Android 1.3.1 independently matches path?query. Its rule matcher uses Kotlin regular expressions. The same optional query suffix therefore preserves direct-root and modal classification on Android; this is a separate source comparison from the iOS one.

The pinned Navigator.start() applies path-configuration properties to the initial tab URL. clear_all ignores the proposed controller and refreshes an existing root, so it can leave that initial empty stack blank. The pinned replaceRoot visits the proposed controller and installs it as the root. That behavior fits both first launch and a later link back to a navigation root.

Both clients recognize context with a modal value and presentation with a replace_root value, alongside pull_to_refresh_enabled, query_string_presentation, and historical_location.

The wider vocabularies differ, which is why the first lowering deliberately uses a small subset:

Key Both clients iOS only Android only
context default, modal — —
presentation default, pop, replace, refresh, clear_all, replace_root none push
modal_style — five sheet styles —
uri — — registered Hotwire fragment destination
title — — More supplies a native title
pull_to_refresh_enabled Boolean; default true, forms false — —

modal_style is an iOS concept with no Android counterpart, so authoring sheet detents would produce output one platform silently ignores. Check each platform's pinned property accessors and destination registry before extending its emitted rules; a platform-specific key need not appear in the other client's configuration.

Android's default URI is hotwire://fragment/web; generated new/edit forms retain it with context: modal and pull_to_refresh_enabled: false. The pinned demo configuration uses the same full-screen form pattern. Core also registers an optional bottom sheet and hotwire://fragment/firstdraft-more. With more than five entries, /__firstdraft_android_more__ selects the latter with title More. Its overflow routes keep default push behavior so Back returns to More; only the first four direct roots receive replace_root. iOS Core recognizes its own /__firstdraft_ios_more__ start location and answers it with the native list, so no path rule names it. Its overflow routes likewise keep default push behavior.

Ordinary browsers render the same Rails new and edit routes as pages. Native path configuration selects their modal presentation; it does not convert browser markup into a modal or remove the Rails header.

Served copies

The generated Rails app serves each selected client's bundled file, so the app and the server share one copy:

# config/routes.rb, before every Scaffold route
get "configurations/ios_v1", to: "native/configurations#ios_v1", as: nil
get "configurations/android_v1", to: "native/configurations#android_v1", as: nil

Native::ConfigurationsController renders ios/FoundationApp/Generated/ios_v1.json or android/app/src/main/assets/json/android_v1.json with render json:, and spec/requests/native/configurations_spec.rb checks that each answers, byte for byte, without signing in. An unselected client gets no route or action. The paths follow Hotwire Native's advice to version one file per platform and to ship both a bundled and a server copy. Joe Masilotti's Hotwire Native for Rails Developers serves them from a ConfigurationsController with ios_v1 and android_v1 actions. The Native namespace keeps that controller apart from the one an Entity named Configuration generates, and listing the routes first keeps that Entity's /configurations/:id from claiming them. Nothing in the app links to these routes, so as: nil gives them no helpers. That also leaves configurations_ios_v1 free for the member helper of an Entity keyed that way.

The actions read no record and need no session, because a client fetches the file at launch, before anyone signs in. The iOS request can carry the web view's cookies; Android's carries none. Each Scaffold controller declares its own sign-in and Action Policy checks, so inheriting ApplicationController adds none; its allow_browser check names only browsers. Docker and Render deploys copy ios/ and android/, because neither .dockerignore nor render.yaml excludes them.

iOS Core passes the bundled file, then /configurations/ios_v1.json, to the pinned Hotwire Native 1.3.0 loader: the bundled file loads synchronously, a cached server copy may replace it, and the server request is asynchronous. Android Core loads the bundled android_v1.json when the application starts and adds /configurations/android_v1.json as the remote source once its activity resolves the origin, because a Debug launch can name a local server. The pinned Android 1.3.1 loader applies a cached server copy, when one exists, instead of the bundled file, then fetches a fresh one. On both platforms a failed request keeps the current rules. The bundled file therefore covers first launch; after one successful request, each launch applies the cached server copy, so an unreachable server leaves the last fetched copy in effect. In the iOS Simulator and an Android emulator, each launch requested its file from the local Rails app and received the bundled copy.

A rule changed in the repository reaches installed apps from the next deploy, on their next launch; their bundled copy waits for a new app build. Every installed build follows the deployed file, including rules an older build was not built for. A Plan change that adds a navigation entry gives its root replace_root, while an older build's tabs were fixed when it was built. In the pinned sources, a link to that root then replaces the current tab's root (iOS, Android). Hotwire Native's advice for a change older builds cannot follow is to point new builds at a _v2 file and keep serving the old one. The Compiler emits only the current Plan's _v1 file and keeps no earlier copy. Open question for the owner: whether generated apps should version their path configuration per build.

Primary source and ownership

The current direction treats the Xcode and Android projects as primary editable source. A generic precompiled First Draft shell would test a different ownership model.

The first owner journey should try an ordinary path to:

The in-progress Hotwire Native agent skill is an output and test surface of this work, not an input or authority for the Capability design.

Distinct delivery lanes

Native evidence is reported by job:

Lane What it can establish What it does not establish
Source generation Editable projects and integration exist Either project builds or runs
Automated build/test The pinned project compiles and its tests pass Interactive behavior or device-only APIs
Simulator preview Rendering, navigation, Bridge, notification UI Signing, phone install, push, distribution
Owner-signed device Real-device lifecycle and eligible capabilities TestFlight/App Store or production receipt
Beta/store Owner-controlled installation Push receipt, background, or tap routing unless exercised
Push lifecycle Registration, receipt, app states, taps, token cleanup Equivalent behavior on other platform

An Apple Account can be used for limited Xcode device testing, while TestFlight, App Store distribution, and advanced app services are associated with Apple Developer Program membership. Current details must be checked against Apple's membership comparison and program benefits when implementing or marketing a lane. The recovered native generation mechanics (repository-only) hold a dated July 2026 snapshot of Google's developer-verification and account landscape. Recheck current requirements before designing or making claims about an Android ownership lane.

Push semantics

Push machinery is activated by an authored application-level fact rather than derived, because no Plan subject can imply it. Decision 0016 (repository-only) records why, and requires an Account-bearing Entity before the fact is valid.

Machinery now, rules later

The first lowering emits the machinery and no notification rules. That split is forced by the causal seam (repository-only): nothing in the Plan can yet say that a successful operation should notify a recipient, because Scaffolds model forms without event emission and state-machine effects reach only same-transition Field assignments.

What the first lowering can emit:

photogram-golden demonstrates the shape with app/controllers/account/push_test_notifications_controller.rb, so this is extraction from working reference source rather than fresh design.

A Foundation that registers devices and can send one test notification proves the whole lifecycle end to end while carrying no rule the format cannot express. When the causal seam closes, notification rules name their own channels and this fact converts to a derived consequence.

Push needs a client to deliver to

ios_push and android_push are separate application-level facts, and each needs its own platform's client. A Plan enabling android_push while declining native.android registers devices that cannot exist, so semantic validation should reject that pairing rather than emit a registration handshake with nothing behind it.

The reverse pairing is legitimate. Shipping a client while deferring that platform's push credentials is a reasonable early position, especially for Android, whose FCM lane has no evidence yet.

Browser Push is not a native transport and carries no client dependency. Noticed 3.0.0 has no web-push delivery method, so web_push would arrive through the separate web-push gem, which the profile has not admitted.

Device storage belongs to the Capability

Action Push Native supplies action_push_native_devices with name, platform, token, and a polymorphic owner. A Plan that also models a device Entity describes the same table twice, so Shinar's device_installation Entity was removed once the delivery facts made registration a Capability concern.

A user-visible device list is a different question. "See and revoke your signed-in devices" is product meaning that Action Push Native does not supply, and an application wanting it would model that surface deliberately rather than as a side effect of enabling push. Nothing in the current format prevents it; no fixture asks for it yet.

Lifecycle coordination

The candidate implementation needs to coordinate:

Registering a token is not end-to-end push proof.

The application-level notification design (repository-only) owns why one recipient should receive push. This Capability owns native endpoint and client lifecycle. Browser Web Push remains a separate adapter.

Revisit

Reconsider Hotwire Native or this topology when generated apps need substantially different client interfaces, shared web UI becomes a continuation liability, platform policy breaks an ownership lane, or controlled evidence favors another native architecture.

References marked “repository-only” name implementation or internal material outside this public guide. They are intentionally not links. Publishing a design does not prove its implementation.