Push & Web Notifications
How notifications reach the Android shade from a Cefrium app -- what works out of the box, what is standards-correct but delivery-gated, and the production path that is validated end-to-end.
| Path | Status | Use it for |
|---|---|---|
Web NotificationsshowNotification() |
Works out of the box | Showing a notification from page or service-worker code. No Firebase, no setup. |
Web PushPushManager.subscribe() |
API present | delivery Google-gated | Standards code that registers with FCM. Production delivery needs Google's allowlist or your own push service. |
| Native FCM Firebase Cloud Messaging |
Production | validated | Delivering push to your app in production, via your own Firebase project. |
Does Cefrium require Google Play Services?
No. The engine - browsing, rendering, the ad-blocker, and Web Notifications display - runs without Google Play Services, including on de-Googled / AOSP devices. GMS is touched only by opt-in feature paths (native FCM push, WebOTP SMS autofill, passkey verification), and each degrades gracefully when GMS is absent.
One honest detail: the SDK declares a single
com.google.android.gms.version manifest value - a manifest-value
check that stops Chromium's WebOTP path from throwing, not a runtime
dependency on Play Services being installed. The fat AAR also vendors the
play-services / Firebase classes (they arrive with Chromium), but they
stay inert unless your app opts into FCM with a google-services.json.
Native push (section 3) is the only path that needs GMS on the device - and only your
app turns it on.
1. Web Notifications - works out of the box
A page asks for permission and posts a notification through its service worker. No Firebase, no extra dependency - the display path lives entirely in the bundled engine.
// page.js
const reg = await navigator.serviceWorker.register('/sw.js');
if (await Notification.requestPermission() === 'granted') {
// `new Notification(...)` is an illegal constructor on Android --
// always go through the service-worker registration:
reg.showNotification('Hello from Cefrium', { body: 'It works.' });
}
Grant the permission natively - answer the prompt, or pre-grant per origin:
// Java / Kotlin (View API)
browser.setPermissionHandler((origin, permissions, cb) -> cb.respond(true));
// or, up front for a known origin:
browser.setContentSetting("https://example.com",
CefriumBrowser.PERMISSION_NOTIFICATIONS, CefriumBrowser.SETTING_ALLOW);
In Compose, the same call is on the navigator:
navigator.setContentSetting(origin, permission, value).
That is the whole integration. The Cefrium Gradle plugin already generates the R classes Chromium's notification code reads, so a standard SDK setup shows web notifications with nothing else added.
2. Web Push - present, standards-correct, delivery is Google-gated
The W3C Push API is exposed and wired through Blink. subscribe()
returns a real subscription (with p256dh + auth keys),
so the engine registers with FCM:
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: <your VAPID public key>,
});
sub.endpoint; // https://fcm.googleapis.com/preprod/wp/<iid>:<token>
A VAPID-signed Web Push to that endpoint is accepted by FCM (HTTP
201) - but does not deliver. The endpoint is on the
preprod channel.
Why - and why no code change fixes it
Chromium derives a GCM "product category" from the build's product name. Any
non-Chrome Chromium build resolves to org.chromium.android. FCM
returns production Web Push only for com.chrome.android
(real Google Chrome) and the pre-production channel for
everything else. Cefrium is a Chromium build, not Chrome, so it is structurally
on preprod. Overriding the product category to impersonate Chrome would violate
Google's Terms (and still fails the production allowlist), so Cefrium does not do
it - this project ships no hacks.
This is the same wall every Chromium fork hits - Brave, for instance, runs its own push service for this exact reason. The honest options for production Web Push are Google granting the project access, or running your own push relay. Neither is a Cefrium code gap. For production push today, use native FCM below - the production transport on Android anyway.
3. Native FCM - the production path
Native FCM uses the device's Google Play Services to register and deliver, so it is on production GCM regardless of the engine's identity.
Validated end-to-end. On a Play-certified emulator, a Cefrium app's Firebase setup returned a real FCM token, and a push sent via FCM HTTP v1 was received and shown in the notification shade.
The one non-obvious part
A normal app adds com.google.firebase:firebase-messaging.
Do not add it here - the Cefrium AAR already vendors the
Firebase classes (they come in with Chromium), so the dependency causes
Duplicate class errors. The AAR vendors the firebase classes
but not the firebase-messaging dependency's manifest entries, so you
declare those yourself. That is the entire integration.
Gradle - plugin + google-services only
plugins {
id 'com.android.application'
id 'com.cefrium'
id 'com.google.gms.google-services' // injects google-services.json
}
dependencies {
implementation 'com.cefrium:cefrium-sdk:0.5.0'
// NO firebase-messaging - it is vendored in the AAR.
configurations.all {
exclude group: 'com.google.guava', module: 'listenablefuture'
exclude group: 'org.jspecify'
exclude group: 'com.squareup.okio' // the AAR vendors okio
}
}
Add your google-services.json to the app module; its
package_name must equal your applicationId.
Manifest - what firebase-messaging's manifest would have contributed
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<application ...>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<service android:name="com.google.firebase.components.ComponentDiscoveryService"
android:exported="false" android:directBootAware="true">
<meta-data android:name="com.google.firebase.components:com.google.firebase.messaging.FirebaseMessagingRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
<meta-data android:name="com.google.firebase.components:com.google.firebase.installations.FirebaseInstallationsRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
<meta-data android:name="com.google.firebase.components:com.google.firebase.FirebaseCommonKtxRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
<meta-data android:name="com.google.firebase.components:com.google.firebase.datatransport.TransportRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter><action android:name="com.google.android.c2dm.intent.RECEIVE" /></intent-filter>
</receiver>
<service android:name="com.google.firebase.messaging.FirebaseMessagingService"
android:exported="false" android:directBootAware="true">
<intent-filter android:priority="-500">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
Get the token, send a push
com.google.firebase.messaging.FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(t -> {
if (t.isSuccessful()) Log.i(TAG, "FCM_TOKEN=" + t.getResult());
});
# Send via FCM HTTP v1 (service-account OAuth)
ACCESS=$(gcloud auth print-access-token)
curl -s -X POST -H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
"https://fcm.googleapis.com/v1/projects/<PROJECT_ID>/messages:send" \
-d '{"message":{"token":"<FCM_TOKEN>",
"notification":{"title":"Cefrium FCM","body":"Native delivery works"}}}'
Play-certified device required.
getToken() goes through Play Services' GCM registration, which only
completes on a certified device. On uncertified hardware (and on
emulator images without the Play Store) it hangs - while
FirebaseInstallations.getId() still works. If getToken()
never returns, that is your device's GMS, not Cefrium. Use a certified phone or a
google_apis_playstore emulator image. On Android 13+, also grant
the runtime POST_NOTIFICATIONS permission or the message is dropped
silently.
Full working sample
The push-demo in cefrium-sample wires all three paths, with a complete manifest, the Gradle exclusions, and token retrieval. Its README is the in-depth reference.