npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@duochat/duo-push

v1.0.0

Published

Official JavaScript/TypeScript SDK for duochat APIs

Downloads

461

Readme

🔔 @duochat/duo-push

The official JavaScript/TypeScript SDK for duochat Push Notifications

npm version License: MIT TypeScript Node.js

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-push

Note: 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_dev claim from your JWT token and automatically routes requests to the correct endpoint. A development token hits duochat-push-dev.onrender.com; a production token hits duochat-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_dev claim. Calling setToken() 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

  1. 🔑 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
  2. 🔀 Use the right token per environment — the SDK auto-detects dev vs prod from the is_dev claim inside the JWT, so just swap the token and the correct endpoint is used automatically
  3. 📱 Register on every app launch — FCM tokens can rotate, so always call registerDevice when the app starts or the user logs in
  4. 🏷️ Use metadata — store app_version, locale, device_model, user_id, etc. to help with targeting and debugging
  5. 📦 Batch your sends — always pass multiple device_ids in one call rather than looping
  6. 🛡️ Always handle errors — wrap calls in try/catch and check error 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