easy-pingv0.7.0

Get in touch

Questions, bug reports, or anything about easy-ping. Either of these reaches me.

Emailteklumo.jembere@gmail.comTelegram@teklumt

For anything others would benefit from, a GitHub issue is better than a DM, because it's searchable.

GitHub

Channels / Email

Edit this page

Email

A Resend provider ships in the box; the EmailProvider interface is open for any other service.

Using Resend

import { resend } from "easy-ping/providers/resend";

channels: {
  email: { provider: resend({ apiKey: process.env.RESEND_API_KEY!, from: "Acme <hi@acme.dev>" }) },
},

It's a plain fetch call to Resend's API — no SDK dependency. Idempotency-Key is set from the delivery id on every request, so a retried send can never land twice on Resend's side even if the retry and the original both eventually succeed.

Failures are classified for you: a 401/403 (bad key) or 422 (invalid recipient) is never retried; a 408/429/5xx is. That split is what keeps a revoked API key from burning all five retry attempts on something that will never succeed.

Subject and template

Declared per notification type, not per provider — the same values work if you swap providers later:

commentReply: defineNotification({
  schema: z.object({ authorName: z.string(), commentId: z.string() }),
  channels: ["email"],
  email: {
    subject: (payload) => `${payload.authorName} replied to you`,
    template: (payload) =>
      `<p>${escapeHtml(payload.authorName)} replied. <a href="/c/${encodeURIComponent(payload.commentId)}">View</a></p>`,
  },
}),

Both functions receive the same typed payload your schema validates — no separate templating data model to keep in sync.

Escape user input

template output is sent exactly as returned. Anything a user typed — a display name, a comment excerpt — goes through escapeHtml (exported from easy-ping), or that user's HTML lands in someone's inbox from your sending domain. react-email and similar renderers escape for you.

A different provider

EmailProvider is a small interface: a name, a send(message), and an isRetryable(error). Anything that can make an HTTP request to your email service qualifies:

type EmailProvider = {
  name: string;
  timeoutMs?: number;
  send(message: {
    to: string;
    subject: string;
    html: string;
    text?: string;
    idempotencyKey: string;
    signal?: AbortSignal;
  }): Promise<{ providerMessageId?: string }>;
  isRetryable?(error: unknown): boolean;
};

idempotencyKey and signal are handed to you already computed — pass them straight through if your provider's API accepts an idempotency header and request cancellation, respectively.