Docs / Search / LingCode Cloud / Push notifications
๐Ÿ“˜ Reference โ— Intermediate Updated 2026-06-11

Push notifications

TL;DR: Web Push lets you notify a user even when your app's tab is closed. It's the browser's standard Web Push (VAPID) flow โ€” it needs HTTPS, a service worker, and the user's permission. The SDK's client.push does two things in the browser: isSupported() to feature-detect, and subscribe() to register your service worker, prompt for permission, and store the user's subscription on the backend. Sending the notification is a separate server-side step โ€” you push to the stored subscription from a function or your hosted app, not from the browser.

Most "notifications" people want are really two halves that look like one. The browser half is about earning permission and capturing where to deliver โ€” that happens in your page, with the user watching. The delivery half is a server poking that saved address later, when something actually happened. LingCode Cloud handles the awkward middle: it stores each user's subscription tied to their account so you can reach them after the tab is long gone. This page covers the browser half end-to-end and points you at the server half.

Why it works this way

A push notification has to arrive when your code isn't running โ€” the user closed the tab hours ago. That's why the standard requires three things up front:

To subscribe, the browser also needs your backend's VAPID public key โ€” the identity that lets the push service trust that pushes claiming to be from you really are. LingCode Cloud manages that key for you; the SDK fetches it under the hood. When the subscription is created, it gets stored on the backend and tied to the signed-in user so you can target them later.

The split that trips people up

The browser subscribes; the server sends. You cannot fire a notification from the page that just called subscribe() โ€” the page only captured the address. Actually pushing a message goes from a function or your hosted app to the user's stored subscription, server-side. Build the subscribe flow first, prove the row lands, then wire up sending.

The browser flow

Inside /try and the Mac preview, window.lingcode already exists. In your own app, create the client first (see the overview), then run these three steps.

Feature-detect first

Not every browser or context can do push. Check before you show any "Enable notifications" UI โ€” there's no point offering a button that can't work.

if (!lingcode.push.isSupported()) {
  // Hide your "Enable notifications" button โ€” push isn't available here.
  return;
}

isSupported() returns a boolean synchronously.

Subscribe the user

Call subscribe() in response to a user gesture (e.g. a button click). It registers your service worker, subscribes through the browser's PushManager, and stores the subscription on the backend against the current user. This call triggers the browser's permission prompt.

const { data, error } = await lingcode.push.subscribe({
  serviceWorker: '/sw.js'
});

if (error) {
  // User denied permission, or push isn't available โ€” fall back gracefully.
  console.warn('Could not enable push:', error);
} else {
  // Subscription saved server-side; the user is now reachable.
}

The opts argument is optional; { serviceWorker?: string } points at your service worker file. Every call returns { data, error }.

Send from the server

This step does not happen in the browser. Once the subscription is stored, push a message to it from a function or your hosted app โ€” anywhere server-side that can reach the user's stored subscription. The browser's only job was to capture permission and the delivery address; everything after that is triggered by your backend when something actually happens.

You must ship a service worker. Push delivery runs through it โ€” there's no push without a /sw.js (or whatever path you pass to subscribe()). Make sure that file is actually served at the path you reference, on the same origin.

Under the hood (REST)

The SDK wraps two endpoints. You rarely call them directly, but knowing the shapes helps when debugging.

Fetch the VAPID public key

The browser needs the backend's VAPID public key before it can subscribe. The SDK fetches it for you:

GET https://lingcode.dev/api/cloud/be/<backend-id>/push/vapid-public

โ†’ { "key": "<vapid-public-key>" }

Store a subscription

After the browser's PushManager produces a subscription object, the SDK posts it to the backend to store it against the current user:

POST https://lingcode.dev/api/cloud/be/<backend-id>/push/subscribe

{
  "subscription": {
    "endpoint": "https://<push-service>/...",
    "keys": {
      "p256dh": "<client-public-key>",
      "auth": "<auth-secret>"
    }
  }
}

Errors

Requirements & gotchas

Where push works

HTTPS only. It works on your hosted app's secure URL โ€” *.run.lingcode.dev or your custom domain โ€” and on http://localhost for development. Plain http:// on any other host will not support push.

Permission is per-user and revocable. The user grants permission for your origin, and they can revoke it in browser settings at any time โ€” your code won't be told. Always feature-detect with isSupported() and handle the not-granted case (the error from subscribe()) instead of assuming a subscription exists.

Native push (iOS & Android)

Web Push covers browsers and installed PWAs. If you also ship a native app, the same backend fans out to Apple's APNs (iOS) and Google's FCM (Android) โ€” push.send reaches web, iOS, and Android subscribers in one call. Delivery has to terminate at Apple and Google, so you bring your own push credentials once, in the backend console โ†’ Push notifications.

iOS โ€” Apple APNs

In the Apple Developer portal โ†’ Keys, create a key with the Apple Push Notifications service (APNs) capability and download the .p8. In the console's iOS push (BYO Apple APNs) card, paste the .p8 contents along with:

Sandbox vs Production must match the build. A device token from a build signed for development only delivers on the Sandbox environment; TestFlight/App-Store tokens only deliver on Production. Mismatched tokens fail with BadDeviceToken and the dead token is pruned automatically.

Android โ€” Firebase FCM

Native Android push must go through Firebase Cloud Messaging (there's no alternative transport). In the Firebase console, create a service account and download its JSON, then paste it into the Android push (BYO Firebase) card. The credential is encrypted at rest.

Registering a native device token

Your app obtains a device token from the OS (APNs registration on iOS, FCM on Android), then registers it against the signed-in user. Hybrid apps (React Native, Capacitor) can use the SDK:

// after the OS hands you a device token
await lingcode.push.registerNativeToken({ kind: 'apns', token: deviceToken }); // iOS
await lingcode.push.registerNativeToken({ kind: 'fcm',  token: deviceToken }); // Android

Pure-native apps can POST the same shape directly โ€” no SDK required:

POST https://lingcode.dev/api/cloud/be/<backend-id>/push/subscribe

{ "subscription": { "kind": "apns", "token": "<device-token>" } }

Sending is unchanged โ€” the same push.send / POST /push/send you use for web fans out to every stored subscription regardless of platform.