@duochat/duo-push
v1.0.0
Published
Official JavaScript/TypeScript SDK for duochat APIs
Downloads
461
Maintainers
Readme
🔔 @duochat/duo-push
The official JavaScript/TypeScript SDK for duochat Push Notifications
Send push notifications, manage devices, and integrate with duochat — all with zero dependencies.
📦 NPM · 📖 Docs · 🐛 Issues · ✉️ Support
✨ Features
- 🚀 Zero dependencies — built on native
fetch, nothing else to install - 🔒 TypeScript first — full type definitions out of the box
- 📱 Multi-platform — iOS, Android, and Web support
- ⚡ Simple API — register devices and fire notifications in minutes
- 🌐 Universal — runs in React Native, Node.js, NestJS, Express, Bun, Deno, and browsers
- 🔀 Auto environment detection — dev vs prod is resolved from the token itself, no extra config needed
📦 Installation
# npm
npm install @duochat/duo-push
# yarn
yarn add @duochat/duo-push
# pnpm
pnpm add @duochat/duo-pushNote: No peer dependencies required. The SDK uses native
fetch, available in Node.js 18+, React Native, Bun, Deno, and all modern browsers.
🗺️ How It Works
This SDK is designed to be used across two separate environments in your stack:
| Method | Environment | Purpose |
| -------------------- | :---------------: | -------------------------------------------------------------- |
| registerDevice() | 📱 Client app | Called when the user opens the app to register their FCM token |
| getDevices() | 🖥️ Backend | Fetch registered devices to decide who to notify |
| sendNotification() | 🖥️ Backend | Trigger push notifications from your server |
⚙️ Configuration
new duochatNotificationClient(config: duochatNotificationClientConfig)| Option | Type | Required | Default | Description |
| ------- | -------- | :------: | ------- | ------------------------------------------------------------------------- |
| token | string | ✅ | — | API token generated from the duochat Dashboard |
🔑 Get your token — log in to app.duochat.io, navigate to your workspace settings, and generate an API token. Treat it like a password — never commit it to source control.
🔀 Environment is automatic — the SDK decodes the
is_devclaim from your JWT token and automatically routes requests to the correct endpoint. A development token hitsduochat-push-dev.onrender.com; a production token hitsduochat-push.onrender.com. You never need to pass an environment flag manually.
// Production token → https://duochat-push.onrender.com
const client = new duochatNotificationClient({
token: process.env.DUO_PUSH_TOKEN!,
});
// Development token → https://duochat-push-dev.onrender.com
const devClient = new duochatNotificationClient({
token: process.env.DUO_PUSH_TOKEN_DEV!,
});📱 Client App Usage
Use this in your React Native, mobile, or web frontend to register the user's device when the app launches.
import { duochatNotificationClient, DuoPushPlatform } from "@duochat/duo-push";
const client = new duochatNotificationClient({
token: "your-api-token", // 👉 Generate at https://app.duochat.io/
});
// Call this after getting the FCM token from Firebase
await client.registerDevice({
fcm_token: "firebase-device-token",
platform: DuoPushPlatform.IOS, // DuoPushPlatform.IOS | .ANDROID | .WEB
metadata: { app_version: "1.0.0" },
});React Native Example
import messaging from "@react-native-firebase/messaging";
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info";
import { duochatNotificationClient, DuoPushPlatform } from "@duochat/duo-push";
const client = new duochatNotificationClient({
token: userAuthToken, // 👉 Generate at https://app.duochat.io/
});
async function registerDeviceForPush() {
// Request permission (iOS)
await messaging().requestPermission();
// Get the FCM token from Firebase
const fcmToken = await messaging().getToken();
// Register it with duochat
const response = await client.registerDevice({
fcm_token: fcmToken,
platform:
Platform.OS === "ios" ? DuoPushPlatform.IOS : DuoPushPlatform.ANDROID,
metadata: {
app_version: "2.0.0",
device_model: DeviceInfo.getModel(),
},
});
console.log("✅ Device registered:", response.device_id);
}
// Call on app startup / after login
useEffect(() => {
registerDeviceForPush();
}, []);Web App Example
import { getToken } from "firebase/messaging";
import { duochatNotificationClient, DuoPushPlatform } from "@duochat/duo-push";
const client = new duochatNotificationClient({
token: userAuthToken, // 👉 Generate at https://app.duochat.io/
});
async function registerWebDevice() {
const permission = await Notification.requestPermission();
if (permission !== "granted") return;
const fcmToken = await getToken(messaging, {
vapidKey: "your-vapid-key",
});
await client.registerDevice({
fcm_token: fcmToken,
platform: DuoPushPlatform.WEB,
metadata: { browser: navigator.userAgent },
});
console.log("✅ Web device registered");
}🖥️ Backend Usage (Node.js / NestJS / Express)
Use this in your server to fetch devices and send notifications. Keep your token server-side only — never expose it in client app bundles.
import { duochatNotificationClient } from "@duochat/duo-push";
const client = new duochatNotificationClient({
token: process.env.DUO_PUSH_TOKEN!, // 👉 Generate at https://app.duochat.io/
});Get All Devices
// Returns Device[] directly — clean and iterable
const devices = await client.getDevices();
const userDevice = devices.find(
(device) => device.metadata?.user_id === userId,
);If you also need the total count, use getDevicesWithMeta():
const { devices, total } = await client.getDevicesWithMeta();
console.log(`📊 Total registered devices: ${total}`);
devices.forEach((device) => {
console.log(`[${device.platform}] ${device.id} — user: ${device.user_id}`);
});Device shape:
interface Device {
id: string;
fcm_token: string;
platform: "ios" | "android" | "web";
metadata?: Record<string, any>;
workspace_id: string;
user_id: string;
created_at: string;
updated_at: string;
}Send a Notification
// 🎯 Send to specific devices
const response = await client.sendNotification({
device_ids: ["device-123", "device-456"],
notification: {
title: "💬 New Message",
body: "John sent you a file",
},
data: {
action: "open_chat",
chat_id: "789",
},
});
console.log(`✅ Delivered: ${response.success_count}`);
console.log(`❌ Failed: ${response.failure_count}`);
// 📢 Broadcast to ALL devices in the workspace (omit device_ids)
await client.sendNotification({
notification: {
title: "📢 Announcement",
body: "Scheduled maintenance tonight at 2 AM",
},
});Update the Auth Token
// Rotate your token when it refreshes
// Note: the base URL is fixed at construction time — swapping tokens
// does not change which environment (dev/prod) is targeted.
client.setToken("new-jwt-token");📖 Full API Reference
registerDevice(data) — 📱 Client
| Field | Type | Required | Description |
| ----------- | --------------------- | :------: | ----------------------------------------- |
| fcm_token | string | ✅ | FCM token from Firebase |
| platform | DuoPushPlatform | ✅ | Device platform |
| metadata | Record<string, any> | ❌ | Any extra info (app version, model, etc.) |
Returns: { message: string, device_id?: string }
getDevices() — 🖥️ Backend
Returns: Device[] — array of devices, directly iterable.
getDevicesWithMeta() — 🖥️ Backend
Returns: { devices: Device[], total: number } — use when you need the total count.
sendNotification(data) — 🖥️ Backend
| Field | Type | Required | Description |
| -------------------- | --------------------- | :------: | ---------------------------------------- |
| device_ids | string[] | ❌ | Target devices. Omit to broadcast to all |
| notification.title | string | ✅ | Notification title |
| notification.body | string | ✅ | Notification body |
| data | Record<string, any> | ❌ | Custom payload passed through to the app |
Returns: { message: string, success_count: number, failure_count: number, results?: [...] }
setToken(token) — Both environments
client.setToken("new-jwt-token");⚠️ The target environment (dev/prod) is locked at construction time based on the initial token's
is_devclaim. CallingsetToken()only updates the auth credential — it does not re-route requests to a different environment.
🛡️ Error Handling
All failures throw a DuoPushError:
import { DuoPushError } from "@duochat/duo-push";
try {
await client.sendNotification({ ... });
} catch (error) {
if (error instanceof DuoPushError) {
console.error("❌ Status:", error.statusCode); // e.g. 401, 404
console.error("💬 Message:", error.message);
console.error("📦 Response:", error.response);
}
}| Property | Type | Description |
| --------------- | --------------------- | -------------------------------------- |
| message | string | Human-readable error description |
| statusCode | number \| undefined | HTTP status code |
| response | any | Raw response body from the API |
| originalError | Error \| undefined | Underlying network error if applicable |
💡 More Examples
🔔 Notify a User Across All Their Devices
async function notifyUser(userId: string, title: string, body: string) {
const devices = await client.getDevices();
const userDeviceIds = devices
.filter((d) => d.user_id === userId)
.map((d) => d.id);
if (userDeviceIds.length === 0) {
console.log("⚠️ No devices found for user");
return;
}
const response = await client.sendNotification({
device_ids: userDeviceIds,
notification: { title, body },
});
console.log(
`✅ Notified user ${userId} on ${response.success_count} device(s)`,
);
}🍎 Target a Specific Platform
async function notifyIOSUsers(title: string, body: string) {
const devices = await client.getDevices();
const iosDeviceIds = devices
.filter((d) => d.platform === DuoPushPlatform.IOS)
.map((d) => d.id);
await client.sendNotification({
device_ids: iosDeviceIds,
notification: { title, body },
data: { deep_link: "myapp://home" },
});
console.log(`🍎 Sent to ${iosDeviceIds.length} iOS device(s)`);
}💬 New Chat Message Notification
async function onNewMessage(
chatId: string,
senderName: string,
recipientDeviceIds: string[],
) {
const response = await client.sendNotification({
device_ids: recipientDeviceIds,
notification: {
title: `💬 ${senderName}`,
body: "Sent you a new message",
},
data: {
action: "open_chat",
chat_id: chatId,
},
});
if (response.failure_count > 0) {
console.warn(`⚠️ ${response.failure_count} notification(s) failed`);
response.results
?.filter((r) => !r.success)
.forEach((r) => console.error(` ↳ ${r.device_id}: ${r.error}`));
}
}📢 Broadcast Announcement
await client.sendNotification({
// No device_ids = send to ALL devices in the workspace
notification: {
title: "🚀 New Feature",
body: "Check out what's new in v3.0",
},
data: { action: "open_changelog" },
});🧩 TypeScript Support
Full types are exported and ready to use:
import type {
RegisterDeviceInput,
SendNotificationInput,
NotificationPayload,
Device,
GetDevicesResponse,
SendNotificationResponse,
duochatNotificationClientConfig,
DuoPushPlatformType,
} from "@duochat/duo-push";
// Platform enum
import { DuoPushPlatform } from "@duochat/duo-push";
await client.registerDevice({
fcm_token: "token",
platform: DuoPushPlatform.IOS, // DuoPushPlatform.IOS | .ANDROID | .WEB
});Available exports:
| Export | Kind | Description |
| --------------------------------- | ----- | ------------------------------------ |
| duochatNotificationClient | class | Main client |
| DuoPushError | class | Error class |
| DuoPushPlatform | const | { IOS, ANDROID, WEB } |
| DuoPushPlatformType | type | Union type of platform values |
| RegisterDeviceInput | type | Input for registerDevice() |
| SendNotificationInput | type | Input for sendNotification() |
| NotificationPayload | type | Notification title/body shape |
| Device | type | Device object shape |
| RegisterDeviceResponse | type | Response from registerDevice() |
| SendNotificationResponse | type | Response from sendNotification() |
| GetDevicesResponse | type | Response from getDevicesWithMeta() |
| duochatNotificationClientConfig | type | Constructor config shape |
✅ Best Practices
- 🔑 Keep backend tokens secret — your token is generated from app.duochat.io and should only ever live on your server via an env variable (e.g.
DUO_PUSH_TOKEN), never in client app bundles - 🔀 Use the right token per environment — the SDK auto-detects dev vs prod from the
is_devclaim inside the JWT, so just swap the token and the correct endpoint is used automatically - 📱 Register on every app launch — FCM tokens can rotate, so always call
registerDevicewhen the app starts or the user logs in - 🏷️ Use metadata — store
app_version,locale,device_model,user_id, etc. to help with targeting and debugging - 📦 Batch your sends — always pass multiple
device_idsin one call rather than looping - 🛡️ Always handle errors — wrap calls in
try/catchand checkerror instanceof DuoPushError
📋 Requirements
| Requirement | Version | | ------------ | ---------------------------------- | | Node.js | 18+ (backend / for native fetch) | | React Native | 0.60+ (client) | | TypeScript | 4.5+ (optional) |
📄 License
MIT © duochat
Need help? Open an issue on GitHub or email us at [email protected]
Made with ❤️ by the duochat team
