Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 195 additions & 179 deletions .cursor/rules/convex_rules.mdc

Large diffs are not rendered by default.

24 changes: 0 additions & 24 deletions .github/workflows/node.js.yml

This file was deleted.

38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Test and lint
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

on:
push:
branches: [main]
pull_request:
branches: ["**"]

jobs:
check:
name: Test and lint
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- name: Node setup
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache-dependency-path: package.json
node-version: "20.x"
cache: "npm"

- name: Install and build
run: |
npm i
npm run build
- name: Publish package for testing branch
run: npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this repo?"
- name: Test
run: |
npm run test
npm run typecheck
npm run lint
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ node_modules
react/package.json
# npm pack output
*.tgz
*.tsbuildinfo
3 changes: 2 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"trailingComma": "es5"
"trailingComma": "all",
"proseWrap": "always"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## 0.2.0

- Adds /test and /\_generated/component.js entrypoints
- Drops commonjs support
- Improves source mapping for generated files
- Changes to a statically generated component API
28 changes: 9 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,37 @@

```sh
npm i
cd example
npm i
npx convex dev
npm run dev
```

## Testing

```sh
rm -rf dist/ && npm run build
npm run clean
npm run build
npm run typecheck
npm run test
cd example
npm run lint
cd ..
npm run test
```

## Deploying

### Building a one-off package

```sh
rm -rf dist/ && npm run build
npm run clean
npm ci
npm pack
```

### Deploying a new version

```sh
# this will change the version and commit it (if you run it in the root directory)
npm version patch
npm publish --dry-run
# sanity check files being included
npm publish
git push --tags
npm run release
```

#### Alpha release

The same as above, but it requires extra flags so the release is only installed with `@alpha`:
or for alpha release:

```sh
npm version prerelease --preid alpha
npm publish --tag alpha
npm run alpha
```
118 changes: 66 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

[![npm version](https://badge.fury.io/js/@convex-dev%2Fresend.svg)](https://badge.fury.io/js/@convex-dev%2Fresend)

This component is the official way to integrate the Resend email service
with your Convex project.
This component is the official way to integrate the Resend email service with
your Convex project.

Features:

- Queueing: Send as many emails as you want, as fast as you want—they'll all be delivered (eventually).
- Batching: Automatically batches large groups of emails and sends them to Resend efficiently.
- Durable execution: Uses Convex workpools to ensure emails are eventually delivered, even in the face of temporary failures or network outages.
- Idempotency: Manages Resend idempotency keys to guarantee emails are delivered exactly once, preventing accidental spamming from retries.
- Queueing: Send as many emails as you want, as fast as you want—they'll all be
delivered (eventually).
- Batching: Automatically batches large groups of emails and sends them to
Resend efficiently.
- Durable execution: Uses Convex workpools to ensure emails are eventually
delivered, even in the face of temporary failures or network outages.
- Idempotency: Manages Resend idempotency keys to guarantee emails are delivered
exactly once, preventing accidental spamming from retries.
- Rate limiting: Honors API rate limits established by Resend.

See [example](./example) for a demo of how to incorporate this hook into your
Expand All @@ -33,7 +37,7 @@ Next, add the component to your Convex app via `convex/convex.config.ts`:

```ts
import { defineApp } from "convex/server";
import resend from "@convex-dev/resend/convex.config";
import resend from "@convex-dev/resend/convex.config.js";

const app = defineApp();
app.use(resend);
Expand Down Expand Up @@ -62,25 +66,28 @@ export const sendTestEmail = internalMutation({
});
```

Then, calling `sendTestEmail` from anywhere in your app will send this test email.
Then, calling `sendTestEmail` from anywhere in your app will send this test
email.

If you want to send emails to real addresses, you need to disable `testMode`.
You can do this in `ResendOptions`, [as detailed below](#resend-component-options-and-going-into-production).
You can do this in `ResendOptions`,
[as detailed below](#resend-component-options-and-going-into-production).

A note on test email addresses:
[Resend allows the use of labels](https://resend.com/docs/dashboard/emails/send-test-emails#using-labels-effectively) for test emails.
For simplicity, this component only allows labels matching `[a-zA-Z0-9_-]*`, e.g. `[email protected]`.
[Resend allows the use of labels](https://resend.com/docs/dashboard/emails/send-test-emails#using-labels-effectively)
for test emails. For simplicity, this component only allows labels matching
`[a-zA-Z0-9_-]*`, e.g. `[email protected]`.

## Advanced Usage

### Setting up a Resend webhook

While the setup we have so far will reliably send emails, you don't have any feedback
on anything delivering, bouncing, or triggering spam complaints. For that, we need
to set up a webhook!
While the setup we have so far will reliably send emails, you don't have any
feedback on anything delivering, bouncing, or triggering spam complaints. For
that, we need to set up a webhook!

On the Convex side, we need to mount an http endpoint to our project to route it to
the Resend component in `convex/http.ts`:
On the Convex side, we need to mount an http endpoint to our project to route it
to the Resend component in `convex/http.ts`:

```ts
import { httpRouter } from "convex/server";
Expand All @@ -100,11 +107,11 @@ http.route({
export default http;
```

If our Convex project is happy-leopard-123, we now have a Resend webhook for
our project running at `https://happy-leopard-123.convex.site/resend-webhook`.
If our Convex project is happy-leopard-123, we now have a Resend webhook for our
project running at `https://happy-leopard-123.convex.site/resend-webhook`.

So navigate to the Resend dashboard and create a new webhook at that URL. Make sure
to enable all the `email.*` events; the other event types will be ignored.
So navigate to the Resend dashboard and create a new webhook at that URL. Make
sure to enable all the `email.*` events; the other event types will be ignored.

Finally, copy the webhook secret out of the Resend dashboard and set it to the
`RESEND_WEBHOOK_SECRET` environment variable in your Convex deployment.
Expand All @@ -116,8 +123,8 @@ Speaking of...

### Registering an email status event handler.

If you have your webhook established, you can also register an event handler in your
apps you get notifications when email statuses change.
If you have your webhook established, you can also register an event handler in
your apps you get notifications when email statuses change.

Update your `sendEmails.ts` to look something like this:

Expand All @@ -144,44 +151,47 @@ Check out the `example/` project in this repo for a full demo.

### Resend component options, and going into production

There is a `ResendOptions` argument to the component constructor to help customize
it's behavior.
There is a `ResendOptions` argument to the component constructor to help
customize it's behavior.

Check out the [docstrings](./src/client/index.ts), but notable options include:

- `apiKey`: Provide the Resend API key instead of having it read from the environment
variable.
- `apiKey`: Provide the Resend API key instead of having it read from the
environment variable.
- `webhookSecret`: Same thing, but for the webhook secret.
- `testMode`: Only allow delivery to test addresses. To keep you safe as you develop
your project, `testMode` is default **true**. You need to explicitly set this to
`false` for the component to allow you to enqueue emails to artibrary addresses.
- `onEmailEvent`: Your email event callback, as outlined above!
Check out the [docstrings](./src/client/index.ts) for details on the events that
are emitted.
- `testMode`: Only allow delivery to test addresses. To keep you safe as you
develop your project, `testMode` is default **true**. You need to explicitly
set this to `false` for the component to allow you to enqueue emails to
artibrary addresses.
- `onEmailEvent`: Your email event callback, as outlined above! Check out the
[docstrings](./src/client/index.ts) for details on the events that are
emitted.

### Optional email sending parameters

In addition to basic from/to/subject and html/plain text bodies, the `sendEmail` method
allows you to provide a list of `replyTo` addresses, and other email headers.
In addition to basic from/to/subject and html/plain text bodies, the `sendEmail`
method allows you to provide a list of `replyTo` addresses, and other email
headers.

### Tracking, getting status, and cancelling emails

The `sendEmail` method returns a branded type, `EmailId`. You can use this
for a few things:
The `sendEmail` method returns a branded type, `EmailId`. You can use this for a
few things:

- To reassociate the original email during status changes in your email event handler.
- To reassociate the original email during status changes in your email event
handler.
- To check on the status any time using `resend.status(ctx, emailId)`.
- To cancel the email using `resend.cancelEmail(ctx, emailId)`.

If the email has already been sent to the Resend API, it cannot be cancelled. Cancellations
do not trigger an email event.
If the email has already been sent to the Resend API, it cannot be cancelled.
Cancellations do not trigger an email event.

### Data retention

This component retains "finalized" (delivered, cancelled, bounced) emails.
It's your responsibility to clear out those emails on your own schedule.
You can run `cleanupOldEmails` and `cleanupAbandonedEmails` from the dashboard,
under the "resend" component tab in the function runner, or set up a cron job.
This component retains "finalized" (delivered, cancelled, bounced) emails. It's
your responsibility to clear out those emails on your own schedule. You can run
`cleanupOldEmails` and `cleanupAbandonedEmails` from the dashboard, under the
"resend" component tab in the function runner, or set up a cron job.

If you pass no argument, it defaults to deleting emails older than 7 days.

Expand All @@ -198,7 +208,7 @@ const crons = cronJobs();
crons.interval(
"Remove old emails from the resend component",
{ hours: 1 },
internal.crons.cleanupResend
internal.crons.cleanupResend,
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
Expand All @@ -212,7 +222,7 @@ export const cleanupResend = internalMutation({
0,
components.resend.lib.cleanupAbandonedEmails,
// These generally indicate a bug, so keep them around for longer.
{ olderThan: 4 * ONE_WEEK_MS }
{ olderThan: 4 * ONE_WEEK_MS },
);
},
});
Expand All @@ -222,9 +232,11 @@ export default crons;

### Using React Email

You can use [React Email](https://react.email/) to generate your HTML for you from JSX.
You can use [React Email](https://react.email/) to generate your HTML for you
from JSX.

First install the [dependencies](https://react.email/docs/getting-started/manual-setup#2-install-dependencies):
First install the
[dependencies](https://react.email/docs/getting-started/manual-setup#2-install-dependencies):

```bash
npm install @react-email/components react react-dom react-email @react-email/render
Expand Down Expand Up @@ -261,8 +273,8 @@ export const sendEmail = action({
>
Click me
</Button>
</Html>
)
</Html>,
),
);

// 2. Send your email as usual using the component
Expand All @@ -276,8 +288,10 @@ export const sendEmail = action({
});
```

> [!WARNING]
> React Email requires some Node dependencies thus it must run in a Convex [Node action](https://docs.convex.dev/functions/actions#choosing-the-runtime-use-node) and not a regular Action.
> [!WARNING] React Email requires some Node dependencies thus it must run in a
> Convex
> [Node action](https://docs.convex.dev/functions/actions#choosing-the-runtime-use-node)
> and not a regular Action.

### Sending emails manually, e.g. for attachments

Expand Down Expand Up @@ -329,7 +343,7 @@ export const sendManualEmail = internalMutation({
],
});
return data.id;
}
},
);
},
});
Expand Down
7 changes: 7 additions & 0 deletions convex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/convex/schemas/convex.schema.json",
"functions": "example/convex",
"codegen": {
"legacyComponentApi": true
}
}
Loading