fcm sends Firebase Cloud Messaging notifications from a Ruby backend
Ruby bindings to Firebase Cloud Messaging (FCM) for Android, iOS or Web
At a glance
- What is it?
- The fcm gem is a Ruby client for Firebase Cloud Messaging that sends to Android, iOS and web devices, covering the HTTP v1 API, device groups and topic messaging, with configurable timeouts and an optional keep-alive connection pool for high-volume senders. It is MIT licensed, the install is one line in a Gemfile, and the newest release is v2.0.3 from August 2026.
- Who is it for?
- fcm suits a Rails or Ruby service that already has Firebase on the client side and needs to push notifications from the server, since it covers the HTTP v1 API, device groups and topic conditions without pulling in a Google SDK, and the credentials can be passed as a path or as a StringIO so they never touch the repository.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 66 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it is, and how small the surface is
The FCM gem lets your ruby backend send notifications to Android and iOS devices via Firebase Cloud Messaging, and the repository is a small, focused client rather than a framework.
Installation is a single gem. Either:
gem 'fcm'in your Gemfile, or gem install fcm from a shell.
The requirements section is short. For Android you will need a device running 2.3 or newer that also has the Google Play Store app installed, or an emulator running Android 2.3 with Google APIs. iOS devices are also supported. And a version of supported Ruby, currently ruby >= 2.4.
That Ruby floor is worth noting. A library that still supports 2.4 is one that has been in use long enough to have accumulated users who cannot upgrade, and the repository's release history fits that story, with 2.0.3 released in August 2026 and 2.0.1 and 2.0.2 both in May 2026.
The API surface is correspondingly narrow. You construct a client with your Firebase credentials and a project id, and then you send. There is no background worker, no retry policy framework, no delivery callback handling, and no abstraction over the FCM error taxonomy. That is a deliberate choice rather than an omission, and it means you will write the surrounding production logic yourself.
The repository layout shows a conventional gem: lib/ for the code, spec/ for the RSpec suite, an fcm.gemspec, a Gemfile, a Rakefile, and the usual .rubocop.yml, .rspec, .hound.yml and LICENSE.txt. The hound configuration is from the era when Hound was the standard hosted linting service for open source Ruby, which is a useful dating signal for the codebase.
The licence is MIT, the primary language is Ruby, and the last push was on 2026-08-18, the day after the v2.0.3 release.
Credentials, and keeping them out of the repository
The client is instantiated with two things, a path to your credentials and your project id:
fcm = FCM.new(
GOOGLE_APPLICATION_CREDENTIALS_PATH,
FIREBASE_PROJECT_ID
)The GOOGLE_APPLICATION_CREDENTIALS_PATH is meant to contain your firebase credentials, and the easiest way to provide them is to pass an absolute path to a file with your credentials:
fcm = FCM.new(
'/path/to/credentials.json',
FIREBASE_PROJECT_ID
)But the README anticipates that you do not want the credentials in your repository, because of their secret nature. The alternative is to pass a StringIO that contains your credentials:
fcm = FCM.new(
StringIO.new(ENV.fetch('FIREBASE_CREDENTIALS')),
FIREBASE_PROJECT_ID
)That second form is the more useful one for a deployed application. Reading the credentials from an environment variable and wrapping them in a StringIO means the secret arrives from whatever secret manager you use, and the file never has to exist on disk. The cost is that you now own the credential refresh, since the StringIO is a snapshot taken when the client was constructed and a rotated service account key will not be picked up by a long-lived process.
Neither form requires the Google Cloud SDK to be installed or the application to run on Google infrastructure. The gem talks to Firebase over HTTPS with the credentials as its bearer token, and the project id identifies the project.
The device requirements deserve a note as well. Android 2.3 with the Play Store app, or an emulator with Google APIs, is what the client is targeting, which is a rather old floor but is a consequence of FCM's own requirements rather than a choice by the gem.
Timeouts and keep-alive, the two production knobs
There are two options in the README that exist because the gem is used in a request path, and they are the parts most worth understanding.
The first is timeouts. Both the read timeout and the TCP connect timeout are configurable via http_options, and both default to DEFAULT_TIMEOUT at 30 seconds:
fcm = FCM.new(
GOOGLE_APPLICATION_CREDENTIALS_PATH,
FIREBASE_PROJECT_ID,
timeout: 10, # response read timeout
open_timeout: 5 # TCP connect timeout — fail fast on stuck handshakes
)Thirty seconds is a long time to block a background job or a web request on, so overriding these is close to mandatory in production. The distinction between the two is the useful part: the read timeout is how long you wait for FCM to answer, and the open timeout is how long you wait for the TCP connection to establish, so a stuck handshake fails fast instead of consuming the whole budget.
The second is HTTP keep-alive. By default each request opens a fresh TCP and TLS connection to FCM, and for high-volume senders this dominates per-request latency. Passing keep_alive_connections: true reuses a thread-local connection per host, backed by the net-http-persistent gem:
fcm = FCM.new(
GOOGLE_APPLICATION_CREDENTIALS_PATH,
FIREBASE_PROJECT_ID,
keep_alive_connections: true,
keep_alive_idle_timeout_seconds: 30, # optional, default 30
keep_alive_pool_size: 1 # optional, default 1
)Two details in the implementation are stated rather than left to be discovered. Net::HTTP is not thread-safe, so connections are cached per thread and per uri. And connections are dropped on error, so half-closed sockets are not reused.
That second point is the kind of detail that saves an afternoon. A half-closed socket that gets reused produces a hang rather than an exception, and the fact that the gem drops the connection on error means you will not hit that path in normal operation.
The idle timeout and pool size both default to 30 seconds and 1 respectively, so the defaults are conservative. A pool size of one per thread is enough for a synchronous sender that makes one request at a time, and would need raising if you were multiplexing requests on one connection, which this gem does not do.
Sending: the HTTP v1 API and what a message looks like
The README has a section headed HTTP v1 API with a link to Firebase's migration guide, which tells you where this gem's emphasis lies.
A v1 message is a hash with a target and a payload:
message = {
'token': "000iddqd", # send to a specific device
'notification': {
title: notification.title_th,
body: notification.body_th,
},
'android': {},
'apns': {
payload: {
aps: {
sound: "default",
category: "#{Time.zone.now.to_i}"
}
}
},
'fcm_options': {
analytics_label: 'Label'
}
}
fcm.send_v1(message) # or fcm.send_notification_v1(message)The target is one of three things, and the README shows all three commented in the example: a token for a specific device, a topic, or a condition that is a boolean expression over topics.
The payload has separate sections for the platform-independent notification, a data section that carries arbitrary JSON to the app, an android section and an apns section for platform-specific options, and fcm_options with an analytics label. The apns payload in the example sets a sound and a category, where the category is derived from the current time so that each notification is distinguishable in the notification centre.
Two send methods are available. send_v1 sends whatever you have built, and send_notification_v1 is the variant for notification messages specifically. The gem does not build the message for you, so the shape of the hash is the FCM v1 contract and you are writing it directly.
That level of control is what makes the gem usable for anything beyond the simplest notification, and it is also the source of its main limitation: because the gem does not model the message, you get no validation and no defaults, and a malformed payload surfaces as an FCM error rather than as a local exception.
Device groups and topics, and the two target models
FCM has two ways of addressing many devices at once, and the gem supports both.
Device group messaging lets you send a single message to multiple instances of an app running on devices belonging to a group. Typically group refers to a set of different devices that belong to a single user, though a group could also represent a set of devices where the app instance functions in a highly correlated manner.
The group is represented by a notification key, and creating one is a separate operation. The key name has to be uniquely named per app in case you have multiple apps for the same project id, which ensures that notifications only go to the intended target app:
params = { key_name: "appUser-Chris",
project_id: "my_project_id",
registration_ids: ["4", "8", "15", "16", "23", "42"] }
response = fcm.create(*params.values)The create method returns the notification_key in the response, and that key is what you send to later. The project_id parameter is the SENDER_ID in your cloud settings.
The membership can then be edited. The add and remove methods take the same shape of parameters, including the notification key and the registration ids:
params = { key_name: "appUser-Chris",
project_id: "my_project_id",
notification_key:"appUser-Chris-key",
registration_ids:["7", "3"] }
response = fcm.add(*params.values)There is a hard limit worth knowing: the maximum number of members allowed for a notification key is 20.
Topic messaging is the other model, and it is a publish and subscribe system. An app server sends a message to multiple devices that have opted in to a particular topic, and one app instance can be subscribed to no more than 2000 topics. Sending to a topic works the same way as sending to a device, using send_v1 with the topic field:
message = {
'topic': "yourTopic", # send to a device group
# ...data
}
fcm.send_v1(message)There is also a helper for the simple case:
response = fcm.send_to_topic("yourTopic",
notification: { body: "This is a FCM Topic Message!"} )Topic conditions are the third option, where the target is a boolean expression over topics rather than a single topic. The README explains the model and the topic field must match the regular expression for a topic name, allowing letters, digits, and a small set of punctuation characters.
The practical difference between the two models matters. A device group is a fixed membership you manage server side, capped at twenty devices. A topic is a subscription the devices opt into, capped at two thousand subscriptions per instance. Which one you need depends on whether the grouping is a property of your account system or a property of the user's device set.
Where fcm fits, and where it stops
The comparison worth making is with Google's own client libraries and with the Firebase Admin SDK.
Against the Google Cloud Ruby client library, the difference is scope. The Google library is a full client for every Google Cloud API and it is the official way to authenticate, and it comes with the whole weight of the Google ecosystem behind it. The fcm gem does one thing, and it does it without asking you to install the SDK or to run on Google infrastructure.
Against a bare HTTP client of your own, the difference is that FCM's HTTP v1 requires OAuth2 with a service account and a signed JWT, which is a small amount of code that is easy to get subtly wrong and tedious to get right. The gem handles that, and the StringIO credential option means you can keep the key out of the filesystem.
The limits are worth naming. The gem requires Ruby 2.4 or above, which is generous for a library but means it is not held to the newest Ruby idioms. It targets only the HTTP v1 API, so if your existing code still speaks the legacy API you have a migration to do, and the README links Firebase's migration guide rather than supporting both. There is no built-in retry, batching, or error classification, so a production deployment has to build that around it. And the keep-alive connection pool is per thread, so a multi-process deployment re-establishes connections per worker.
On the API surface, there is one design decision that will affect anyone extending it. The create, add and remove methods take their parameters as a hash and are called with the splat on its values, so the order of the keys in the hash determines the order of the arguments. That is a fragile arrangement, and it is worth being careful about when constructing those hashes programmatically.
On maintenance, the picture is active: v2.0.1 and v2.0.2 on the same day in May 2026, v2.0.3 in August 2026, and the last push on 2026-08-18. The repository has a CI workflow for tests and a RuboCop configuration, and the issues tracker on GitHub is where you would report a problem.
Editorial conclusion
fcm suits a Rails or Ruby service that already has Firebase on the client side and needs to push notifications from the server, since it covers the HTTP v1 API, device groups and topic conditions without pulling in a Google SDK, and the credentials can be passed as a path or as a StringIO so they never touch the repository. It is the wrong tool if you have not yet moved to the HTTP v1 API, because the README sends you to Firebase's migration guide, and it is the wrong tool for very high send volumes from a single process unless you turn on keep_alive_connections, since a fresh TCP and TLS handshake per request dominates the latency otherwise. Before you adopt it, read the thread-safety note, because Net::HTTP is not thread-safe and the connections are cached per thread and per host.
Frequently asked questions
How do I install the fcm gem?
Add gem 'fcm' to your Gemfile, or run gem install fcm. The library requires ruby >= 2.4. For Android you need a device running 2.3 or newer with the Google Play Store app, or an emulator with Google APIs, and iOS devices are supported too.
How do I pass Firebase credentials to the fcm gem?
You construct the client with FCM.new(credentials, project_id). The credentials can be an absolute path to a credentials file, or a StringIO containing them, which the README recommends if you do not want the secret in your repository. The StringIO form is typically built from an environment variable.
How do I set timeouts and enable keep-alive connections in fcm?
Pass timeout and open_timeout to set the response read timeout and the TCP connect timeout, both of which default to DEFAULT_TIMEOUT of 30 seconds. Pass keep_alive_connections: true to reuse a thread-local connection per host, backed by net-http-persistent, with optional keep_alive_idle_timeout_seconds and keep_alive_pool_size. Net::HTTP is not thread-safe, so connections are cached per thread and uri, and are dropped on error.
What is the difference between device groups and topics in fcm?
A device group has a fixed membership that you manage server side with create, add and remove, and the maximum number of members allowed for a notification key is 20. A topic is a publish and subscribe channel that devices opt into, and one app instance can be subscribed to no more than 2000 topics. You can also target a boolean condition over several topics.
Which FCM API does the fcm gem use?
The HTTP v1 API. The README points to Firebase's migration guide for moving to HTTP v1, and the sending methods are send_v1 for a full message and send_notification_v1 for notification messages, plus a send_to_topic helper for the simple topic case.
What licence is the fcm gem released under?
MIT, with the LICENSE.txt file at the repository root. The most recent release is v2.0.3 from 2026-08-17, and the last push to the repository was on 2026-08-18.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/decision-labs-fcm)Was this useful?