Boost App Engagement with Rails + Hotwire Push Notifications

Learn how to integrate push notifications into your Hotwire Native + Rails app and drive user engagement without the complexity of native code. A step-by-step guide for teams building modern, high-performance mobile apps.

Boost App Engagement with Rails + Hotwire Push Notifications

Push Notifications with FCM

Integrate Push Notifications in Rails and Android: Send a Welcome Message on App Start

Step 1: Firebase Setup

1. Go to Google Firebase Console

  • Create a new project (or use an existing one)
  • Go to Project Settings > Cloud Messaging
  • Copy your Server Key (FCM Legacy)
  • Copy your Sender ID

2. Add Firebase to an Android App & Rails app

  • Download google-services.json
  • Place it inside your Android module’s app/ directory.
  • Modify android/app/build.gradle:
  • Get the hotwire-native-app-firebase-adminsdk-**.json file from the Firebase, by following these steps. Go to Firebase > your project > Service account > Generate new Private key. Once you generate it, add this file to your Rails project in the config folder.

Step 2: Rails FCM Setup

  1. Add the FCM gem to Gemfile and configure:

# Gemfile
gem 'fcm'

2. Create the device_token model

2.a. Generate the Model

Run these commands:


rails generate model DeviceToken token:string user:references platform:string
rails db:migrate

2.b. Model: app/models/device_token.rb

class DeviceToken < ApplicationRecord
  belongs_to :user, optional: true
  validates :token, presence: true, uniqueness: true
  validates :platform, inclusion: { in: %w[android ios], allow_nil: true }
end

2.c. Add Association to User Model app/models/user.rb:

has_many :device_tokens, dependent: :destroy

2.d. Let's implement the push notification service that we used in controller. Create file services/ push_notification_service.rb

require "fcmpush"

class PushNotificationService
def initialize(devices)
  @client  = Fcmpush.new(ENV["FIREBASE_PROJECT_ID"])
  @devices = devices
end

def send_notifications
  return if devices.empty?

  payloads = device_tokens.map do |token|
    {
      message: {
        token: token,
        notification: {
          title: "Welcome Back!",
          body: "We're glad to see you again. Let's get started!"
        }
      }
    }
  end
  payloads.each do |payload|
    response = client.push(payload)
  end
end

private

attr_reader :client, :devices

def device_tokens
  devices.pluck(:token)
end
end


2.e. Create device_token controller app/controllers/api/device_token_controller.rb

class Api::DeviceTokensController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
def create
  user = User.first
  token = DeviceToken.find_or_initialize_by(token: params[:token], user_id: user.id)
  token.platform = params[:platform]

  if token.save
    PushNotificationService.new(user.device_tokens).send_notifications
    render json: { success: true }
  else
    render json: { errors: token.errors.full_messages }, status: :unprocessable_entity
  end
end
end

2.f. Update your route path as per your new action and controller. Update routes.rb

  namespace :api do
    post "device_tokens", to: "device_tokens#create"
  end

2.g. For that, here I am binding my stimulus controller, which I am triggering on the connect action.

  <div data-controller="push-notification"></div>

2.h. Add config/Initializer/fcm_push.rb

Ensure you replace the JSON file path with your own

# FCM_CLIENT = FCM.new(Rails.application.credentials.dig(:fcm, :server_key))
Fcmpush.configure do |config|
config.json_key_io = "#{Rails.root}/config/hotwire-native-app-firebase-adminsdk-**.json"
config.server_key= ENV["FCM_SERVER_KEY"]
end

Step 3: Setup FCM and Notification on Android side

3.a. Add the dependencies to the build.gradle.kts file inside the app directory.

  // Firebase
    implementation ("com.google.firebase:firebase-messaging:24.0.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

3.b. Add a plugin in the build.gradle.kts file app level

id("com.google.gms.google-services")

3.c. Add dependencies in the build.gradle.kts file in the root level

buildscript {
    dependencies {
        classpath("com.google.gms:google-services:4.3.15")
    }
}

3.d. Create MyFierbaseServices.kt in the java/com/example/appnameMyFirebaseService.kt

package com.example.yourapp

# import all dependencies

class MyFirebaseService : FirebaseMessagingService() {

    // Called when a new FCM token is generated
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        sendTokenToBackend(token)
    }

    // Sends the FCM token to your Rails backend
    fun sendTokenToBackend(token: String) {
        val url = "your API url"
        val mediaType = "application/json".toMediaTypeOrNull()
        val body = json.toString().toRequestBody(mediaType)

        val request = Request.Builder()
            .url(url)
            .post(body)
            .addHeader("Content-Type", "application/json")
            .build()

        Thread {

              val response = OkHttpClient().newCall(request).execute()

      }.start()
    }

    // Called when a push notification is received while app is in foreground
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        showNotification(remoteMessage.notification?.title, remoteMessage.notification?.body)
    }

    private fun showNotification(title: String, message: String) {
        val notificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val channelId = "default_channel"

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Default Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(this, channelId)
            .setContentTitle(title)
            .setContentText(message)
            .setSmallIcon(R.drawable.ic_notification)
            .build()

        notificationManager.notify(0, notification)
    }
}

3.e. Add that service code in the MainActivity.kt 

package com.example.hotwirenativeblog

# import your packages here


class MainActivity : HotwireActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // ✅ Step 1: Initialize Firebase
        FirebaseApp.initializeApp(this)
        // ✅ Apply IME insets
        findViewById<View>(R.id.main_nav_host).applyDefaultImeWindowInsets()

        // ✅ Request location permissions
        requestLocationPermissions()

        // ✅ Fetch FCM token manually and send it to backend
        FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val token = task.result
             
                MyFirebaseService().sendTokenToBackend(token)
            }     

}

    private fun requestLocationPermissions() {
        val permissions = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )

        val permissionsToRequest = permissions.filter { permission ->
            ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
        }

        if (permissionsToRequest.isNotEmpty()) {
            locationPermissionLauncher.launch(permissionsToRequest.toTypedArray())
        } else {
            Log.d("Location", "Location permissions already granted")
        }
    }

    override fun navigatorConfigurations() = listOf(
        NavigatorConfiguration(
            name = "main",
            startLocation = "your root path API url", // Your local network IP address
            navigatorHostId = R.id.main_nav_host
        )
    )

}

3.f Create a notification icon in the res/drawableIc_notification.xml and also add the required permission.

Once you set up this, we are good to go live!


At Humive, we help businesses launch high-performance mobile apps using Rails + Hotwire Native with features like push notifications, real-time updates, and native navigation—all from a single codebase.

👉 Let’s talk about how we can turn your idea into a launch-ready mobile experience. Please contact here