Skip to content

plop-email/plop

Repository files navigation

Plop Logo

Plop

Reliable email tests. Finally.

Programmable inboxes for devs and QA. Send emails to Plop, fetch via API, assert in tests.
No mail server. No flaky waits. Just a simple API.

Website | Dashboard | Docs | API

Built with Bun Next.js 16 TypeScript License


The Problem

Testing email flows is painful:

  • Flaky waits - Polling IMAP servers with arbitrary timeouts
  • Infrastructure overhead - Running Mailhog, MailCatcher, or real SMTP servers
  • Unpredictable routing - Shared test inboxes causing race conditions
  • No observability - Email sits outside your product stack

The Solution

Plop makes email testing deterministic and fast:

1. Send to Plop     →  qa+signup@in.plop.email
2. Fetch via API    →  GET /v1/messages/latest
3. Assert in tests  →  Extract OTP, verify links, check content

That's it. Email arrives, you fetch it as JSON, your test passes.


Features

Core

  • Mailboxes + Tags - Route flows with mailbox+tag addresses (e.g., qa+login@in.plop.email)
  • REST API - List, filter, and fetch emails. Poll for the latest message
  • Scoped API Keys - Full access, email-only, or mailbox-scoped permissions
  • Instant Delivery - Email arrives → stored → available via API instantly
  • Message Retention - 14-90 days depending on plan

Security

  • Private Inboxes - Mailboxes require API key authentication
  • Strict Routing - Unknown mailboxes are rejected and never stored
  • Mailbox Isolation - API keys can be scoped to specific mailboxes
  • Reserved Names - 180+ protected mailbox names prevent phishing/impersonation

Developer Experience

  • Works with Cypress, Playwright, and any Node test framework
  • Simple REST API - just fetch() with an API key
  • OpenAPI documentation with interactive Scalar UI
  • Comprehensive filtering by mailbox, tag, date range, and search query

Quick Start

1. Create an Account

Sign up at app.plop.email and create a team. Your first mailbox is auto-generated.

2. Get Your API Key

Navigate to Team Settings → API Keys and create a key.

3. Use Plop in Your Tests

Cypress

// Use a Plop address for signup
cy.get('[data-testid="email-input"]').type('qa+signup@in.plop.email')
cy.get('[data-testid="submit"]').click()

// Fetch the verification email
cy.request({
  method: 'GET',
  url: 'https://api.plop.email/v1/messages/latest?mailbox=qa&tag=signup',
  headers: { Authorization: 'Bearer YOUR_API_KEY' },
}).then(({ body }) => {
  // Extract OTP from email content
  const otp = body.data.textContent?.match(/\b\d{6}\b/)?.[0]
  cy.get('[data-testid="otp-input"]').type(otp)
})

Playwright

import { test, expect } from '@playwright/test'

test('user can verify email', async ({ page, request }) => {
  await page.fill('[data-testid="email"]', 'qa+verify@in.plop.email')
  await page.click('[data-testid="submit"]')

  // Fetch verification email from Plop
  const res = await request.get(
    'https://api.plop.email/v1/messages/latest?mailbox=qa&tag=verify',
    { headers: { Authorization: `Bearer ${process.env.PLOP_API_KEY}` } }
  )

  const { data } = await res.json()
  const otp = data.textContent?.match(/\b\d{6}\b/)?.[0]

  await page.fill('[data-testid="otp"]', otp)
  await expect(page.locator('[data-testid="success"]')).toBeVisible()
})

Node.js / Any Framework

const response = await fetch(
  'https://api.plop.email/v1/messages/latest?mailbox=qa&tag=password-reset',
  {
    headers: { Authorization: `Bearer ${process.env.PLOP_API_KEY}` },
  }
)

const { data } = await response.json()
console.log(data.subject)     // "Reset your password"
console.log(data.textContent) // Plain text body
console.log(data.htmlContent) // HTML body

API Reference

Messages

Endpoint Description
GET /v1/messages List messages with filters
GET /v1/messages/latest Get most recent matching message
GET /v1/messages/:id Get specific message by ID

Query Parameters

Parameter Description
mailbox Filter by mailbox name
tag Filter by tag (from mailbox+tag@)
from Filter by date range start
to Filter by date range end
q Full-text search query

Response Example

{
  "data": {
    "id": "msg_abc123",
    "mailbox": "qa",
    "tag": "signup",
    "from": "noreply@yourapp.com",
    "to": "qa+signup@in.plop.email",
    "subject": "Verify your email",
    "textContent": "Your verification code is 482913",
    "htmlContent": "<html>...</html>",
    "receivedAt": "2024-01-15T10:30:00Z"
  }
}

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Your App      │────▶│  Cloudflare     │────▶│   Plop Worker   │
│  sends email    │     │  Email Routing  │     │   (apps/inbox)  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                                        │
                                                        ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Your Tests    │◀────│   Messages API  │◀────│   PostgreSQL    │
│  fetch emails   │     │   GET /latest   │     │   + R2 Storage  │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Data Flow:

  1. Email arrives at Cloudflare Email Routing
  2. Cloudflare Worker (apps/inbox) receives and parses email
  3. Metadata stored in PostgreSQL, raw .eml in R2
  4. Messages API serves emails with filtering and search
  5. Your tests fetch the latest matching email

Tech Stack

This is a Turborepo monorepo with Bun workspaces.

Apps

App Stack Purpose
apps/app Next.js 16 (App Router) Product dashboard with inbox management
apps/web Next.js Marketing website
apps/docs Next.js + Fumadocs Documentation site
apps/api Bun + Hono + tRPC Backend API with OpenAPI docs
apps/inbox Cloudflare Worker Email ingestion worker

Packages

Package Purpose
@plop/db Drizzle schema + Postgres client with read replicas
@plop/supabase Supabase clients (server/client/middleware/job)
@plop/ui Shared UI components (shadcn-style)
@plop/kv Upstash Redis + rate limiting
@plop/jobs Trigger.dev background jobs
@plop/email React Email templates
@plop/billing Polar.sh billing integration
@plop/logger Pino structured logger

Hosting

Service Provider
Auth, Database, Storage Supabase
Web & App Vercel
Email Routing & Workers Cloudflare
Background Jobs Trigger.dev
Rate Limiting Upstash Redis

Development

Prerequisites

Setup

# Clone the repository
git clone https://github.com/plop-email/plop.git
cd plop

# Install dependencies
bun install

# Start local Supabase
bun dev:supabase

# Run migrations
bun migrate

# Start all apps in parallel
bun dev

Available Scripts

# Development
bun dev              # Run all apps in parallel
bun dev:app          # Product app at localhost:3000
bun dev:web          # Marketing site at localhost:3001
bun dev:docs         # Documentation at localhost:3002
bun dev:api          # API at localhost:3003
bun dev:email        # Email preview at localhost:3004

# Quality
bun run format       # Format with Biome
bun run lint         # Lint with Biome + sherif
bun run typecheck    # TypeScript check all workspaces

# Database
bun migrate          # Run migrations
bun seed             # Generate and run seeds
bun reset            # Reset local database
bun generate         # Regenerate DB types

Local URLs

Service URL
Product App http://localhost:3000
Marketing Site http://localhost:3001
Documentation http://localhost:3002
API http://localhost:3003
Email Preview http://localhost:3004
Supabase Studio http://localhost:54323

Pricing

Plan Price Mailboxes Emails/Month Retention
Starter $6.99/mo 1 5,000 14 days
Pro $49/mo 10 60,000 90 days
Enterprise Contact us Unlimited Unlimited Custom

All plans include unlimited tags and a 14-day free trial. No credit card required.


Use Cases

E2E Testing

Verify OTP codes, password reset links, and signup confirmations in your automated test suite.

QA Automation

Test email flows in CI/CD pipelines with deterministic, isolated inboxes per test run.

Transactional Email Testing

Assert on receipts, notifications, and order confirmations before shipping to production.

Onboarding Flow Testing

Verify welcome emails, verification flows, and user activation sequences.


Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the AGPL-3.0 license for non-commercial use.


Links


Built with care by Alex Vakhitov at Comonad Limited