@zavudev/convex
v0.1.0
Published
Zavu multi-channel messaging component for Convex
Maintainers
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/convexQuick 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_here3. 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/webhookSubscribe to these events:
message.queuedmessage.sentmessage.deliveredmessage.readmessage.failedmessage.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
