DOCS LLMs

RailsFast Native - Notifications

RailsFast sends push notifications to the RailsFast iOS and Android apps with action_push_native (APNs for iOS, FCM for Android). The template ships every part of it: the device table, the endpoint the apps register their token with, the bridge controller that asks the app for its token, an "Enable notifications" row on the native Settings page, and a delivery rule that stops pushes to a phone the moment someone signs out of it.

IMPORTANT

Nothing is on until you configure it. A fresh app has no push credentials, so it renders no bridge, never asks a phone for a token and sends nothing. Each platform switches on by itself once its settings are complete.

The full reference lives in your app: docs/native/push.md (setup, sending, upgrading) and the "Push contract" section of docs/native/server-contract.md (the wire protocol and the compatibility matrix for shells). This page is the short version.

Sending a notification

ApplicationPushNotification
  .new(title: "Your report is ready", body: "Tap to open it", data: { path: "/reports/42" })
  .deliver_later_to(user.application_push_devices)
  • data: { path: "/..." } is the screen the app opens when the notification is tapped. Use url: instead for a full URL; an external one opens in the system browser. Both apps read url first, then path.
  • deliver_later_to takes one device, an array or a relation, and queues one job per device. deliver_to(device) sends to one device right away.
  • Everything else the gem offers works as its README describes: badge:, sound:, thread_id:, apple_data:, google_data:, .silent, with_apple / with_google, before_delivery callbacks.

Who receives it

A device (ApplicationPushDevice) belongs to the user and to the signed-in session that registered it (the Session row of the sessions gem). It is deliverable only while that session is live and its platform is configured. Every way a sign-in ends goes through the sessions gem: signing out, the invitation "switch account" button, "Sign out" on another device in Settings, an admin revoke, and revoke-on-password-change. So:

  • Signing out of the app stops its notifications at once, with no help from the app.
  • A queued notification carries the owner and the session it was queued for. Right before the send, one query requires both, together with a live session. A notification queued for one person never reaches the next person who signs in on that phone.
  • A send already past that check when the sign-out commits still goes out. Apple and Google cannot recall a notification once they have it.

Signing in again binds the device to the new sign-in when the app registers again. A current shell does that on its next page load. A shell built before RailsFast iOS and Android 1.1.0 is never asked passively (it would show the permission prompt), so after each new sign-in it receives nothing until the person taps Settings, Enable notifications.

PushDevicePruneJob runs daily from config/recurring.yml and deletes devices that have been undeliverable for 90 days.

Setup

iOS (APNs)

  1. In Apple Developer, Certificates, Identifiers & Profiles, Keys, create a key with Apple Push Notifications service (APNs). Download the .p8 file (Apple lets you download it once) and note its Key ID.

  2. Add both to the credentials of each environment that sends:

    production:
      action_push_native:
        apns:
          key_id: ABC123DEFG
          encryption_key: |
            -----BEGIN PRIVATE KEY-----
            ...
            -----END PRIVATE KEY-----
  3. The team id and topic (bundle id) come from the first iOS app id your apple-app-site-association file already publishes: IOS_APP_LINK_APP_IDS, then native.ios.app_ids in config/railsfast/railsfast.yml, then IOS_APP_LINK_TEAM_ID + IOS_APP_LINK_BUNDLE_ID (or native.ios.team_id + bundle_id). If the app that receives pushes is not that first app id, set APNS_TOPIC to its bundle id.

  4. In the iOS app, the Push Notifications capability is already declared (aps-environment comes from RAILSFAST_APS_ENVIRONMENT: development for Debug, production for Release). Enable Push Notifications on the App ID in Apple Developer.

Builds installed from Xcode get development tokens, which only the APNs sandbox accepts. config/push.yml connects to the sandbox in the development environment and to production APNs everywhere else, so test a production server with a TestFlight build, on a real device.

Android (FCM)

  1. In the Firebase console, add Firebase to your project and register the Android app with its exact package name. Put the downloaded google-services.json at app/google-services.json in the Android repo (or in a build-type folder under app/src/ if you use one Firebase project per environment). The Google Services plugin is applied only when such a file exists, and without it the shell answers every registration with {registration_error: true}.

  2. In Project settings, Service accounts, choose Generate new private key and paste the whole JSON into credentials:

    production:
      action_push_native:
        fcm:
          encryption_key: |
            { "type": "service_account", "project_id": "your-project", ... }
  3. Tell the server the Firebase project id: ANDROID_FIREBASE_PROJECT_ID, or native.android.firebase_project_id in config/railsfast/railsfast.yml (the ENV var wins). It must be the project the service account and google-services.json belong to.

RailsFast Android posts every notification to one channel, railsfast_push_notifications, created at launch and named in the manifest so background pushes do not land in Firebase's generic channel. Keep that id stable: Android stores the person's notification settings under it.

Development

The credentials sample only has a production block for push. To try push from your machine, add the same action_push_native block under development: and set the identity settings above.

Check it

bin/railsfast doctor lists APNs and FCM as optional services while their credentials are still the sample's placeholders. From a console:

Railsfast::Push.configured_platforms # => ["apple", "google"]

To send yourself a test notification, open the app, sign in, tap Settings, Enable notifications, then:

user = User.find_by!(email: "[email protected]")
user.application_push_devices.deliverable # your phone, bound to its live session
ApplicationPushNotification.new(title: "Hello", body: "From the console", data: { path: "/settings" })
  .deliver_later_to(user.application_push_devices)

[push] log lines trace each step: device.register outcome=created|reused|stale, delivery.skipped, device.pruned (a dead token) and devices.pruned.

How registration works

Two bridge components and one endpoint:

  • native-push answers register and open_settings. Both shells register it.
  • native-push-settings is a capability signal. A shell that advertises it never prompts on a passive register and can open the app's notification settings. RailsFast iOS and Android register it from 1.1.0.

The web side only talks to them on a signed-in native page whose shell advertises native-push, whose platform is configured on the server, and that has a tracked session (HotwireNativeHelper#native_push_available?).

Web sends When
register {request_permission: false} on every page load, only to shells with native-push-settings (the hidden native/_push_bridge element)
register {request_permission: true} when the person taps "Enable notifications" (native/_push_settings_row, on the Settings page)
open_settings {} when the person taps "Open settings", offered only to shells with native-push-settings

The shell replies to that exact message with {token, platform, permission_granted: true}, {permission_granted: false, can_request_again} or {registration_error: true}. The web then posts the token to POST /native/push_tokens with {token, platform}, from inside the WebView, with its normal session cookie and CSRF token. There is no separate mobile API authentication.

Status Meaning
201 stored, bound to this user and this sign-in
200 the token was known; now bound to this user and sign-in
401 not signed in
409 this request's sign-in has ended; nothing was stored
422 unknown platform, or a missing or blank token
503 push is not configured for that platform; nothing was stored

DELETE /native/push_tokens with {token} forgets a token for the signed-in user. Shells do not need to call it at sign-out: the device stops being deliverable when the session ends.

The Settings row hides once this sign-in is registered and comes back if the OS permission is later switched off. Its labels live in the locale files under railsfast.native.push.*.

Upgrading an app from before push

  1. Merge the template and run bin/rails db:migrate. Four migrations run: CreatePushDevicesForNativeShells, AddSessionToPushDevices, ValidatePushDeviceSessionForeignKey and IndexPushDeviceTokensUniquely. On an app that never had push they create the table and nothing else. If the unique-index migration loses a race to a concurrent duplicate, run bin/rails db:migrate again: it drops the invalid index, removes the duplicates and builds again.
  2. config/railsfast/railsfast.yml is merge-guarded, so the new native.android.firebase_project_id key does not arrive by itself. Add it by hand, or use the ENV var.
  3. Rebuild the apps from RailsFast iOS and Android 1.1.0 or later to get passive registration and "Open settings". Installed older apps keep working through the Settings row.

An app that built push on its own before this release gets add/add conflicts in the push models, job, bridge partial and Stimulus controller. Follow "If the app already built push" in the template's docs/native/push.md: merge the models by meaning, move senders to ApplicationPushNotification, delete your own token endpoint, and decide what to do with jobs queued before the upgrade.

Verification

Before a release, on real devices:

  • Settings, Enable notifications shows the OS prompt once, then hides the row.
  • A notification with data: { path: "/settings" } opens the app on /settings; an external url: opens the system browser.
  • Signing out stops delivery: queue a notification after sign-out and check the [push] delivery.skipped log line.
  • Switching notifications off in the OS settings brings the Settings row back, with "Open settings" on a current shell.

References