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

@zavudev/convex

v0.1.0

Published

Zavu multi-channel messaging component for Convex

Readme

@zavudev/convex

A Convex component for Zavu - the unified multi-channel messaging API. Send SMS, WhatsApp, and Email messages with full local sync and real-time status updates.

Features

  • Multi-channel messaging: SMS, WhatsApp, and Email through a single API
  • Full local sync: Messages and contacts stored in your Convex database
  • Real-time updates: Webhook-driven status updates (queued, sent, delivered, read, failed)
  • WhatsApp 24-hour window tracking: Automatic tracking of conversation windows
  • Type-safe: Full TypeScript support

Installation

npm install @zavudev/convex

Quick Start

1. Add the component to your app

In your convex/convex.config.ts:

import { defineApp } from "convex/server";
import zavu from "@zavudev/convex/convex.config";

const app = defineApp();
app.use(zavu);

export default app;

2. Set up your environment variable

Add your Zavu API key to your Convex environment:

npx convex env set ZAVU_API_KEY your_api_key_here

3. Create messaging functions

In your convex/messaging.ts:

import { Zavu } from "@zavudev/convex";
import { components } from "./_generated/api";
import { action, query } from "./_generated/server";
import { v } from "convex/values";

const zavu = new Zavu(components.zavu, {
  httpPrefix: "/zavu",
});

export const sendSMS = action({
  args: { to: v.string(), text: v.string() },
  handler: async (ctx, args) => {
    const apiKey = process.env.ZAVU_API_KEY;
    if (!apiKey) throw new Error("ZAVU_API_KEY is required");

    return await zavu.sendMessage(ctx, {
      to: args.to,
      channel: "sms",
      text: args.text,
      ZAVU_API_KEY: apiKey,
    });
  },
});

export const sendWhatsApp = action({
  args: {
    to: v.string(),
    text: v.string(),
    templateId: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const apiKey = process.env.ZAVU_API_KEY;
    if (!apiKey) throw new Error("ZAVU_API_KEY is required");

    // Check if WhatsApp window is open (required for non-template messages)
    const isWindowOpen = await zavu.isWhatsAppWindowOpen(ctx, args.to);
    if (!isWindowOpen && !args.templateId) {
      throw new Error("WhatsApp 24-hour window is closed. Use a template.");
    }

    return await zavu.sendMessage(ctx, {
      to: args.to,
      channel: "whatsapp",
      messageType: args.templateId ? "template" : "text",
      text: args.text,
      content: args.templateId ? { templateId: args.templateId } : undefined,
      ZAVU_API_KEY: apiKey,
    });
  },
});

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    text: v.string(),
    htmlBody: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const apiKey = process.env.ZAVU_API_KEY;
    if (!apiKey) throw new Error("ZAVU_API_KEY is required");

    return await zavu.sendMessage(ctx, {
      to: args.to,
      channel: "email",
      subject: args.subject,
      text: args.text,
      htmlBody: args.htmlBody,
      ZAVU_API_KEY: apiKey,
    });
  },
});

export const listMessages = query({
  args: {
    status: v.optional(v.string()),
    channel: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    return await zavu.listMessages(ctx, args);
  },
});

export const getConversation = query({
  args: { phoneNumber: v.string() },
  handler: async (ctx, args) => {
    return await zavu.getConversation(ctx, args.phoneNumber);
  },
});

4. Register webhook routes

In your convex/http.ts:

import { httpRouter } from "convex/server";
import { Zavu } from "@zavudev/convex";
import { components } from "./_generated/api";

const http = httpRouter();

const zavu = new Zavu(components.zavu, {
  httpPrefix: "/zavu",
});

zavu.registerRoutes(http);

export default http;

5. Configure webhooks in Zavu dashboard

Set your webhook URL to:

https://your-convex-deployment.convex.site/zavu/webhook

Subscribe to these events:

  • message.queued
  • message.sent
  • message.delivered
  • message.read
  • message.failed
  • message.inbound

API Reference

Zavu Class

const zavu = new Zavu(components.zavu, {
  httpPrefix: "/zavu", // Optional, defaults to "/zavu"
});

Methods

sendMessage(ctx, options)

Send a message via SMS, WhatsApp, or Email.

const result = await zavu.sendMessage(ctx, {
  to: "+1234567890",
  channel: "sms", // "sms" | "whatsapp" | "email"
  text: "Hello!",
  ZAVU_API_KEY: apiKey,

  // Optional fields
  messageType: "text", // "text" | "template" | "image" | etc.
  subject: "Email subject", // Required for email
  htmlBody: "<p>HTML content</p>", // Optional for email
  content: { templateId: "..." }, // For templates/media
  metadata: { orderId: "123" },
  idempotencyKey: "unique-key",
});

Returns:

{
  messageId: string;      // Local Convex ID
  zavuMessageId: string;  // Zavu API ID
  status: string;
  channel: string;
}

getMessage(ctx, messageId)

Get a message by its local Convex ID.

const message = await zavu.getMessage(ctx, messageId);

getMessageByZavuId(ctx, zavuMessageId)

Get a message by its Zavu API ID.

const message = await zavu.getMessageByZavuId(ctx, "msg_abc123");

listMessages(ctx, options)

List messages with optional filters.

const { items, nextCursor } = await zavu.listMessages(ctx, {
  status: "delivered",
  channel: "whatsapp",
  direction: "outbound",
  limit: 50,
  cursor: previousCursor,
});

listIncoming(ctx, options) / listOutgoing(ctx, options)

List inbound or outbound messages only.

const incoming = await zavu.listIncoming(ctx, { limit: 20 });
const outgoing = await zavu.listOutgoing(ctx, { limit: 20 });

getConversation(ctx, counterparty, options)

Get all messages with a specific phone number or email.

const { items } = await zavu.getConversation(ctx, "+1234567890", {
  limit: 100,
});

getContact(ctx, identifier)

Get a contact by phone number or email.

const contact = await zavu.getContact(ctx, "+1234567890");

listContacts(ctx, options)

List all contacts.

const { items, nextCursor } = await zavu.listContacts(ctx, {
  limit: 50,
});

isWhatsAppWindowOpen(ctx, phoneNumber)

Check if the WhatsApp 24-hour conversation window is open.

const isOpen = await zavu.isWhatsAppWindowOpen(ctx, "+1234567890");
if (!isOpen) {
  // Must use a template message
}

registerRoutes(http)

Register webhook HTTP routes.

const http = httpRouter();
zavu.registerRoutes(http);

Data Model

Messages Table

Messages are stored with full details:

| Field | Type | Description | |-------|------|-------------| | zavuMessageId | string | Zavu API message ID | | providerMessageId | string? | Provider's message ID | | direction | "inbound" | "outbound" | Message direction | | from | string | Sender identifier | | to | string | Recipient identifier | | channel | "sms" | "whatsapp" | "email" | Delivery channel | | status | string | Current status | | messageType | string | Type of message | | text | string? | Message text | | subject | string? | Email subject | | content | object? | Media/template content | | errorCode | string? | Error code if failed | | errorMessage | string? | Error description | | cost | number? | Message cost | | metadata | object? | Custom metadata |

Contacts Table

Contacts are automatically created/updated when messages are sent or received:

| Field | Type | Description | |-------|------|-------------| | identifier | string | Phone number or email | | identifierType | "phone" | "email" | Type of identifier | | profileName | string? | WhatsApp profile name | | availableChannels | string[] | Available channels | | whatsappWindowExpiresAt | number? | Window expiration | | messageCount | number | Total messages | | lastMessageAt | number? | Last message timestamp | | lastInboundAt | number? | Last inbound timestamp |

Webhook Events

The component handles these webhook events automatically:

| Event | Description | |-------|-------------| | message.queued | Message queued for delivery | | message.sent | Message sent to provider | | message.delivered | Message delivered to recipient | | message.read | Message read by recipient (WhatsApp) | | message.failed | Message delivery failed | | message.inbound | Inbound message received |

Testing

For testing with convex-test:

import { convexTest } from "convex-test";
import { test } from "@zavudev/convex/test";
import schema from "./schema";

const t = convexTest(schema);
t.registerComponent("zavu", test.component, test.modules);

TypeScript Types

All types are exported from the main package:

import type {
  Channel,
  MessageStatus,
  MessageType,
  Direction,
  SendMessageOptions,
  Message,
  Contact,
  SendResult,
  ZavuConfig,
  PaginatedResult,
  WebhookEvent,
} from "@zavudev/convex";

License

MIT