npm package: @redspringxyz/convex-loops-component
Description: A Loops transactional email component for Convex.
Latest version: 0.1.3

# Loops Convex Component

This component integrates the Loops transactional email service with your Convex project.

Features

- Queueing: Enqueue as many emails as you need; they are sent reliably in the background.
- Batching: Groups pending emails and sends them efficiently to Loops.
- Durable execution: Uses Convex workpools to retry and resume on transient failures.
- Idempotency: Ensures each email is sent exactly once using idempotency keys.
- Rate limiting: Honors Loops API limits via a built‑in rate limiter.
- Webhooks: Processes Loops webhooks and updates email status automatically.

See the example app in `./example` for a working end‑to‑end setup.

## Installation

```bash
npm install @redspringxyz/convex-loops-component
```

## Setup

1. Add the component in `convex/convex.config.ts`:

```ts
import { defineApp } from "convex/server";
import loops from "@redspringxyz/convex-loops-component/convex.config";

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

export default app;
```

2. Configure environment variables in your Convex deployment:

- `LOOPS_API_KEY` (required): Loops API key for transactional sends.
- `LOOPS_WEBHOOK_SECRET` (optional): When set, webhook requests are signature‑verified.

## Usage

Initialize the client in your Convex code and send a transactional email by template ID.

```ts
// convex/emails.ts
import { components, internal } from "./_generated/api";
import { internalAction, internalMutation } from "./_generated/server";
import { Loops, vOnEmailEventArgs } from "@redspringxyz/convex-loops-component";

export const loops = new Loops(components.loops, {
  // Optional: receive callbacks after status updates
  onEmailEvent: internal.emails.handleEmailEvent,
});

export const sendWelcome = internalAction({
  args: {},
  handler: async (ctx) => {
    // Use your Loops transactional template ID
    await loops.sendEmail(ctx, {
      transactionalId: "your-transactional-id",
      email: "user@example.com",
      dataVariables: { name: "Ada" }, // Variables used by your Loops template
    });
  },
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs, // { id: EmailId; event: EmailEvent }
  handler: async (ctx, args) => {
    // Example: react to delivery / bounce / complaint
    console.log("Email update:", args.id, args.event.eventName);
  },
});
```

## Webhook

Add an HTTP route that forwards Loops webhook events to the component. In the Loops dashboard, create a webhook pointing to this URL and enable email events. If you set `LOOPS_WEBHOOK_SECRET`, requests are signature‑verified.

```ts
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { loops } from "./emails";

const http = httpRouter();

http.route({
  path: "/loops-webhook",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    return await loops.handleLoopsWebhook(ctx, req);
  }),
});

export default http;
```

Example endpoint URL: `https://<your-project>.convex.site/loops-webhook`.

## API

Sending

- `loops.sendEmail(ctx, { transactionalId, email, dataVariables, headers? })`
  - Sends a single transactional email using the given template and variables.
  - Returns an `EmailId` that you can use to query status or cancel.

Status and retrieval

- `loops.status(ctx, emailId)` → `{ status, errorMessage, complained, opened } | null`
- `loops.get(ctx, emailId)` → full record including:
  - `to`, `transactionalId`, `dataVariables`, optional `headers`
  - `status` (one of `waiting`, `queued`, `cancelled`, `sent`, `delivered`, `delivery_delayed`, `bounced`, `failed`)
  - `errorMessage?`, `complained`, `opened`, `providerMessageId?`, `finalizedAt`, `createdAt`

Cancellation

- `loops.cancelEmail(ctx, emailId)`
  - Works only before the email has been handed off (while `waiting` or `queued`).

Webhook handling

- `loops.handleLoopsWebhook(ctx, req)`
  - Parses and verifies (if configured) Loops webhook events and updates stored status.
  - Triggers `onEmailEvent` if provided.

Options

- `apiKey?: string` — defaults to `process.env.LOOPS_API_KEY`.
- `webhookSecret?: string` — defaults to `process.env.LOOPS_WEBHOOK_SECRET`.
- `initialBackoffMs?: number` — base delay for retries (default 30s).
- `retryAttempts?: number` — max retry attempts for background work (default 5).
- `onEmailEvent?: FunctionReference<{ id: EmailId; event: EmailEvent }>` — mutation to invoke after updates.

## Data Retention and Cleanup

Finalized emails (e.g., delivered, bounced, cancelled, failed) are kept for a limited time. Schedule cleanup jobs to remove old data.

```ts
// convex/crons.ts
import { cronJobs } from "convex/server";
import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";

const crons = cronJobs();

crons.interval(
  "Remove old emails from the loops component",
  { hours: 1 },
  internal.crons.cleanupLoops
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export const cleanupLoops = internalMutation({
  args: {},
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, components.loops.lib.cleanupOldEmails, {
      olderThan: ONE_WEEK_MS,
    });
    await ctx.scheduler.runAfter(
      0,
      components.loops.lib.cleanupAbandonedEmails,
      {
        olderThan: ONE_WEEK_MS,
      }
    );
  },
});

export default crons;
```

## Example App

The `example/` directory shows a complete Convex app wired to Loops:

- Component mounting (`convex.config.ts`)
- Webhook routing (`convex/http.ts`)
- Sending and event callbacks (`convex/example.ts`)
- Cleanup crons (`convex/crons.ts`)

## Notes

- This component sends one transactional email per Loops API call; the component handles batching and rate limiting internally.
- Manual “send your own HTTP request” APIs have been removed; use `sendEmail`.