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 🙏

© 2025 – Pkg Stats / Ryan Hefner

posti-email

v0.1.6

Published

Self-hosted email tracking for AWS SES with open and click tracking

Readme

posti-email

Self-hosted email tracking with open and click tracking. Supports AWS SES and Cloudflare Workers Email. No third-party tracking services required.

Features

  • 📧 Send HTML + text emails via AWS SES (recommended) or Cloudflare
  • 📊 Track opens with a transparent pixel
  • 🔗 Track clicks with automatic link rewriting
  • 📈 Monitor delivery, bounce, and complaint events (AWS SES only)
  • 🗄️ Prisma integration with ready-to-use schema
  • ⚡ Next.js App Router handlers included
  • 🔌 Framework-agnostic core handlers
  • 💰 Cost-effective: AWS SES starts at $0.10 per 1,000 emails

Note: AWS SES is recommended for full feature support. Cloudflare Workers Email works for opens/clicks tracking but lacks webhook support for delivery/bounce/complaint events.

Installation

npm install posti-email
npm install @prisma/client  # peer dependency

Setup

1. Add Prisma Models

Copy the models from node_modules/posti-email/prisma/schema-snippet.prisma into your schema.prisma.

⚠️ Important: You MUST use Prisma migrations to create the tables. The enums (EmailStatus, EmailEventType) are required and won't work if you create SQL tables manually without proper enum types.

Note: You have full control over your database - use any Prisma-supported database (PostgreSQL, MySQL, SQLite, etc.) and host it anywhere you want (AWS RDS, Railway, Supabase, local, etc.). posti-email just uses your PrismaClient:

model Email {
  id                String       @id
  to                String
  from              String
  subject           String
  providerMessageId String?
  status            EmailStatus  @default(SENT)
  createdAt         DateTime     @default(now())
  events            EmailEvent[]
}

model EmailEvent {
  id        String      @id @default(cuid())
  email     Email       @relation(fields: [emailId], references: [id])
  emailId   String
  type      EmailEventType
  meta      Json?
  createdAt DateTime     @default(now())
}

enum EmailStatus {
  SENT
  DELIVERED
  BOUNCED
  COMPLAINT
}

enum EmailEventType {
  SENT
  DELIVERED
  BOUNCED
  COMPLAINT
  OPEN
  CLICK
}

Then run:

npx prisma generate
npx prisma migrate dev --name add_posti_email_models

2. Initialize Prisma Client

In your app initialization code (e.g., a lib/db.ts file):

For regular Node.js environments:

import { PrismaClient } from "@prisma/client";
import { setPrismaClient } from "posti-email";

const prisma = new PrismaClient();
setPrismaClient(prisma);

export { prisma };

For Vercel/Serverless environments (Recommended):

import { createServerlessPrisma } from "posti-email";

// Automatically creates singleton Prisma Client and configures posti-email
const prisma = createServerlessPrisma();

export { prisma };

Alternative manual setup:

import { PrismaClient } from "@prisma/client";
import { setPrismaClient } from "posti-email";

// Prisma Client singleton for serverless (prevents connection exhaustion)
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

setPrismaClient(prisma);

export { prisma };

The singleton pattern prevents connection pool exhaustion in serverless environments where functions are frequently created and destroyed.

3. Environment Variables

Add these to your .env:

# Email Provider (AWS SES by default)
# To use AWS SES, provide these credentials:
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key

# posti-email Configuration (Required)
# This is your app's base URL - where your tracking endpoints are hosted
# Example: https://myapp.com or https://api.myapp.com
POSTI_TRACKING_BASE_URL=https://myapp.com

# Optional: Only needed if using HTTP send endpoint (/api/send)
# Generate a secure random string (e.g., using `openssl rand -hex 32`)
# This key protects your send endpoint from unauthorized use
POSTI_INTERNAL_SEND_KEY=your_random_secret_key_here

Where to Get AWS Credentials

  1. AWS Access Keys:

    • Go to AWS Console → IAM → Users → Create user or select existing
    • Under "Security credentials" tab, click "Create access key"
    • Grant SES permissions (e.g., AmazonSESFullAccess policy)
    • Copy the Access Key ID and Secret Access Key
  2. AWS Region:

    • Choose the region where your SES is set up (e.g., us-east-1, eu-west-1)
    • Verify your domain/email in SES before sending

What is POSTI_TRACKING_BASE_URL?

This is your application's public URL where the tracking endpoints (/api/open, /api/click) are hosted. It's used to build tracking pixel URLs and click redirect URLs in emails.

Examples:

  • If your app is at https://myapp.com, set: POSTI_TRACKING_BASE_URL=https://myapp.com
  • If your API is at https://api.myapp.com, set: POSTI_TRACKING_BASE_URL=https://api.myapp.com

The tracking endpoints will be:

  • {POSTI_TRACKING_BASE_URL}/api/open (for open tracking)
  • {POSTI_TRACKING_BASE_URL}/api/click (for click tracking)

What is POSTI_INTERNAL_SEND_KEY?

This is an optional security key for the HTTP send endpoint. If you mount the /api/send route, set this to prevent unauthorized email sending.

Generate a secure key:

# Using openssl
openssl rand -hex 32

# Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

When calling the endpoint, include it in the Authorization header:

curl -X POST https://myapp.com/api/send \
  -H "Authorization: Bearer YOUR_GENERATED_KEY" \
  -H "Content-Type: application/json" \
  -d '{...}'

Note: If you don't set POSTI_INTERNAL_SEND_KEY, the endpoint will be unprotected. Only use the HTTP endpoint for internal services, or always set this key.

4. Add API Routes (Next.js)

Email Events Webhook Handler (AWS SES only)

// app/api/email-events/route.ts
import { createSesWebhookHandler } from "posti-email/next";

export const POST = createSesWebhookHandler();

⚠️ AWS SES Only: This endpoint only works with AWS SES. Cloudflare Workers Email does not support webhook notifications.

Configure your AWS SES to send webhooks to this endpoint:

  1. Create an SNS topic in AWS
  2. In SES, configure "Configuration sets" → "Event destinations"
  3. Add your SNS topic as a destination for: Delivery, Bounce, Complaint
  4. Subscribe your webhook endpoint (/api/email-events) to the SNS topic
  5. Alternatively, use SES Event Publishing to send events directly to HTTPS

Open Tracking Pixel

// app/api/open/route.ts
import { createOpenPixelHandler } from "posti-email/next";

export const GET = createOpenPixelHandler();

Click Redirect Handler

// app/api/click/route.ts
import { createClickRedirectHandler } from "posti-email/next";

export const GET = createClickRedirectHandler();

Optional: HTTP Send Endpoint

// app/api/send/route.ts
import { createSendHandler } from "posti-email/next";

export const POST = createSendHandler();

If you use this endpoint, make sure to set POSTI_INTERNAL_SEND_KEY and include it in the Authorization header:

curl -X POST https://myapp.com/api/send \
  -H "Authorization: Bearer your_secret_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "from": "[email protected]",
    "subject": "Hello",
    "html": "<p>Hello world</p>"
  }'

Usage

Sending Emails

import { sendEmail } from "posti-email";

const result = await sendEmail({
  to: "[email protected]",
  from: "[email protected]",
  subject: "Reset your password",
  html: `<p>Click <a href="https://myapp.com/reset?token=abc">here</a> to reset.</p>`,
  text: "Reset your password: https://myapp.com/reset?token=abc",
});

console.log(result.id); // UUID of the email

The function automatically:

  1. Generates a unique message ID
  2. Injects a tracking pixel for opens
  3. Rewrites all links for click tracking
  4. Sends via your configured provider (AWS SES by default)
  5. Records the initial email and SENT event in your database

Getting Email Statistics

import { getEmailStats } from "posti-email";

const stats = await getEmailStats(emailId);

if (stats) {
  console.log(`Opens: ${stats.opens}`);
  console.log(`Clicks: ${stats.clicks}`);
  console.log(`Status: ${stats.status}`);
  console.log(`Last event: ${stats.lastEventAt}`);
}

Framework-Agnostic Handlers

If you're not using Next.js, you can use the core handlers directly:

import {
  sesWebhookCore,
  openPixelCore,
  clickRedirectCore,
} from "posti-email/server";

// Adapt to your framework
app.post("/email-events", async (req, res) => {
  const result = await sesWebhookCore(req.body);
  res.json(result);
});

How It Works

  1. Sending: When you call sendEmail(), posti-email:

    • Generates a UUID for the email
    • Injects a 1x1 tracking pixel: <img src="{baseUrl}/api/open?id={messageId}" />
    • Rewrites all links to go through your click endpoint: {baseUrl}/api/click?id={messageId}&url={originalUrl}
    • Sends via your configured provider (AWS SES or Cloudflare)
    • Stores the email record in your database
  2. Open Tracking: When the email is opened, the tracking pixel loads and hits your /api/open endpoint, which logs an OPEN event.

  3. Click Tracking: When a link is clicked, it first goes to your /api/click endpoint, logs a CLICK event, then redirects to the original URL.

  4. Provider Events (AWS SES only): AWS SES sends webhooks (via SNS) for delivery, bounce, and complaint events. Your /api/email-events endpoint processes these and updates the email status. Note: Cloudflare Workers Email does not support webhooks, so delivery/bounce/complaint tracking won't work with Cloudflare.

Email Providers

AWS SES (Recommended) ⭐

Why AWS SES?

  • ✅ Full feature support (opens, clicks, delivery, bounce, complaints)
  • ✅ Reliable webhook notifications
  • ✅ Production-ready and battle-tested
  • ✅ Excellent deliverability
  • ✅ Comprehensive analytics

Pricing:

  • Free tier: 62,000 emails/month when sent from EC2
  • After free tier: $0.10 per 1,000 emails
  • Full pricing details

Setup:

  1. Get AWS Credentials:

    • Create an IAM user with SES permissions
    • Generate Access Key ID and Secret Access Key
    • Add to your .env as shown in step 3
  2. Verify Your Domain/Email:

    • Go to AWS SES Console → Verified identities
    • Verify your sending domain or email address
    • This is required before sending emails
  3. Set Up Webhooks (for delivery/bounce/complaint tracking):

    • Create an SNS topic in AWS
    • In SES, configure "Configuration sets" → "Event destinations"
    • Add your SNS topic as a destination for:
      • Delivery
      • Bounce
      • Complaint
    • Subscribe your webhook endpoint (/api/email-events) to the SNS topic
    • Alternatively, use SES Event Publishing to send events directly to HTTPS

Cloudflare Workers Email

Status: Currently in private beta

⚠️ Feature Limitations:

Cloudflare Workers Email does NOT support webhook notifications for delivery, bounce, or complaint events. This means:

✅ What Works:

  • Sending emails
  • Open tracking (via pixel)
  • Click tracking (via redirect)

❌ What Doesn't Work:

  • Delivery status webhooks (status stays as SENT, never becomes DELIVERED)
  • Bounce tracking via webhooks (won't detect bounced emails)
  • Complaint tracking via webhooks (won't detect spam complaints)

When to Use Cloudflare:

  • You're already using Cloudflare Workers
  • You only need opens/clicks tracking (most common use case)
  • You prefer the Cloudflare ecosystem
  • Cost is a primary concern

When to Use AWS SES:

  • You need full event tracking (delivery, bounce, complaints)
  • You want production-grade reliability
  • You need comprehensive analytics
  • Recommended for most use cases

Setup:

  1. Join the Cloudflare Email Sending beta: https://developers.cloudflare.com/workers-email/
  2. Add an email binding to your Cloudflare Worker
  3. Configure posti-email to use Cloudflare:
import { setEmailProvider, CloudflareProvider } from "posti-email";

// In your Cloudflare Worker
export default {
  async fetch(request, env, ctx) {
    // Set up Cloudflare provider on first request
    if (!env.emailProvider) {
      setEmailProvider(new CloudflareProvider(env.EMAIL));
    }
    
    // Now you can use sendEmail() normally
    // Open and click tracking still work!
    const result = await sendEmail({...});
  }
}

Note: Cloudflare Email Sending is still in beta and lacks webhook support. For full feature parity (especially delivery/bounce/complaint tracking), AWS SES is recommended.

TypeScript

Full TypeScript support with exported types:

import type {
  SendEmailOptions,
  SendEmailResult,
  EmailStats,
} from "posti-email";

Custom Database Adapters

By default, posti-email uses Prisma. If you need a custom database adapter, you can implement the DatabaseAdapter interface:

import { setDatabaseAdapter } from "posti-email";
import type { DatabaseAdapter } from "posti-email";

const myAdapter: DatabaseAdapter = {
  email: {
    // Implement methods
  },
  emailEvent: {
    // Implement methods
  },
};

setDatabaseAdapter(myAdapter);

Serverless/Vercel Compatibility

Prisma Client Setup

Vercel and other serverless platforms require special Prisma Client initialization to prevent connection pool exhaustion. Use the singleton pattern shown in step 2 above.

Connection Pooling

For production serverless deployments, consider using a connection pooler:

Recommended options:

  • Supabase: Built-in connection pooling
  • Prisma Data Proxy: Prisma's official solution
  • PgBouncer (PostgreSQL): Traditional connection pooler

Example with Prisma Data Proxy:

// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL") // Use Data Proxy URL
  directUrl = env("DIRECT_URL")   // Direct connection for migrations
}

AWS SDK in Serverless

The AWS SDK (@aws-sdk/client-ses) is serverless-friendly out of the box. It handles connection pooling and reuse automatically.

Environment Variables on Vercel

Make sure to add all required environment variables in Vercel's dashboard:

  • AWS_REGION
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • POSTI_TRACKING_BASE_URL
  • DATABASE_URL (for Prisma)
  • POSTI_INTERNAL_SEND_KEY (optional)

Troubleshooting

"Too many connections" errors:

  • Use the singleton Prisma Client pattern
  • Enable connection pooling (Prisma Data Proxy, PgBouncer, etc.)
  • Reduce connection pool size in DATABASE_URL: ?connection_limit=5&pool_timeout=10

"Prisma client not set" errors:

  • Make sure setPrismaClient() is called before any posti-email functions
  • In Next.js, ensure initialization happens in a module that's imported early
  • For API routes, initialize in a shared module that loads before route handlers
  • For Vercel/serverless: Use createServerlessPrisma() helper which automatically configures posti-email

Enum type errors (EmailStatus, EmailEventType):

  • DO NOT create SQL tables manually - use Prisma migrations
  • Run npx prisma migrate dev to create tables with proper enum types
  • If you created tables manually, drop them and use Prisma migrations instead
  • Prisma enums are database-specific types and won't work without proper migration

License

MIT