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.
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. Useurl:instead for a full URL; an external one opens in the system browser. Both apps readurlfirst, thenpath.deliver_later_totakes 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_deliverycallbacks.
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)
-
In Apple Developer, Certificates, Identifiers & Profiles, Keys, create a key with Apple Push Notifications service (APNs). Download the
.p8file (Apple lets you download it once) and note its Key ID. -
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----- -
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, thennative.ios.app_idsinconfig/railsfast/railsfast.yml, thenIOS_APP_LINK_TEAM_ID+IOS_APP_LINK_BUNDLE_ID(ornative.ios.team_id+bundle_id). If the app that receives pushes is not that first app id, setAPNS_TOPICto its bundle id. -
In the iOS app, the Push Notifications capability is already declared (
aps-environmentcomes fromRAILSFAST_APS_ENVIRONMENT:developmentfor Debug,productionfor 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)
-
In the Firebase console, add Firebase to your project and register the Android app with its exact package name. Put the downloaded
google-services.jsonatapp/google-services.jsonin the Android repo (or in a build-type folder underapp/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}. -
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", ... } -
Tell the server the Firebase project id:
ANDROID_FIREBASE_PROJECT_ID, ornative.android.firebase_project_idinconfig/railsfast/railsfast.yml(the ENV var wins). It must be the project the service account andgoogle-services.jsonbelong 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-pushanswersregisterandopen_settings. Both shells register it.native-push-settingsis a capability signal. A shell that advertises it never prompts on a passiveregisterand 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
- Merge the template and run
bin/rails db:migrate. Four migrations run:CreatePushDevicesForNativeShells,AddSessionToPushDevices,ValidatePushDeviceSessionForeignKeyandIndexPushDeviceTokensUniquely. 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, runbin/rails db:migrateagain: it drops the invalid index, removes the duplicates and builds again. config/railsfast/railsfast.ymlis merge-guarded, so the newnative.android.firebase_project_idkey does not arrive by itself. Add it by hand, or use the ENV var.- 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 externalurl:opens the system browser. - Signing out stops delivery: queue a notification after sign-out and check the
[push] delivery.skippedlog line. - Switching notifications off in the OS settings brings the Settings row back, with "Open settings" on a current shell.
References
- Rails
action_push_native: https://github.com/rails/action_push_native - Apple, Registering your app with APNs: https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns
- Apple, token-based connection to APNs: https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
- Firebase, Get started with FCM in Android apps: https://firebase.google.com/docs/cloud-messaging/android/get-started
- Firebase, authorize send requests: https://firebase.google.com/docs/cloud-messaging/auth-server
- Android Developers, notification runtime permission: https://developer.android.com/develop/ui/views/notifications/notification-permission