Your email, but it burns.
A privacy-first disposable email service built with Next.js. Generate a temporary inbox, receive real emails in real-time, and burn everything when you're done — no accounts, no server-side storage.
- Disposable inboxes — Random address generated per device, no sign-up required
- Real-time delivery — Emails arrive instantly via Server-Sent Events (SSE) backed by Redis pub/sub
- Client-side encryption — Email content is AES-GCM encrypted in the browser before being stored in IndexedDB
- Auto-burn timer — Inbox self-destructs after 5 minutes, 1 hour, 24 hours, or never
- Burn on command — Instantly wipe an address and all its emails
- Address history — Browse emails from past addresses; clear all history permanently
- OTP & verify-link detection — One-time codes and verification links are automatically flagged
- Attachment support — Attachments stored as encrypted data in IndexedDB
- Zero server-side storage — The server is a relay only; emails are never persisted on the backend
- Dark / light theme
- Responsive — Mobile-friendly layout: email on one line, compact burn timer (flame + countdown + duration) with Copy/New as icon-only; rounded email box with draining border
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| UI | HeroUI, Radix UI, shadcn/ui, Tailwind CSS v4 |
| Icons | Phosphor Icons |
| Email provider | Resend (inbound webhooks) |
| Real-time | Server-Sent Events (SSE) + Redis pub/sub (ioredis) |
| Local storage | IndexedDB via idb |
| Encryption | Web Crypto API — AES-GCM 256-bit |
| Language | TypeScript |
User opens app
└─> Device config created in IndexedDB (email address + burn timer)
└─> SSE connection opened to /api/email/stream/[address]
└─> Stream handler subscribes to Redis channel poof:email:[address]
Sender sends email to anything@yourdomain.com
└─> Resend receives it via inbound MX
└─> Resend POSTs to /api/email/receive (webhook)
└─> Server validates payload
└─> Publishes to Redis channel poof:email:[address]
└─> All subscribed SSE streams forward the event to their clients
Browser receives SSE event
└─> Email content encrypted with AES-GCM device key
└─> Stored in IndexedDB
└─> UI updates instantly
User burns the inbox
└─> All emails deleted from IndexedDB
└─> Device config wiped
└─> New address generated on next visit
- Node.js 18+
- A Redis instance (local, Upstash, Railway, etc.)
- A Resend account with a verified domain and inbound email enabled
git clone https://github.com/yourusername/poof.git
cd poof
pnpm installcp .env.local.example .env.local| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
Yes | Your Resend API key |
NEXT_PUBLIC_EMAIL_DOMAIN |
Yes | Your verified domain (e.g. yourdomain.com) |
REDIS_URL |
Yes | Redis connection URL (e.g. redis://localhost:6379) |
WEBHOOK_SECRET |
No | Random secret for webhook request validation |
NEXT_PUBLIC_APP_URL |
No | Your deployed app URL (use an ngrok URL for local inbound) |
NEXT_PUBLIC_GITHUB_URL |
No | If set, shows a GitHub link in the footer |
For local development:
# macOS
brew install redis && brew services start redis
# Docker
docker run -p 6379:6379 redis:alpineFor production, use a hosted Redis service such as Upstash (free tier available) and set REDIS_URL to the connection string they provide.
- Go to Resend → Domains → your domain → Inbound
- Add the MX record Resend provides
- Set the webhook URL to
https://yourapp.com/api/email/receive - Optionally set a webhook secret and add it to
WEBHOOK_SECRET
pnpm devOpen http://localhost:3000.
Local inbound emails: Resend can't reach
localhost. Use a tunnel like ngrok and setNEXT_PUBLIC_APP_URLto your tunnel URL; point the Resend webhook athttps://your-ngrok-url.ngrok-free.app/api/email/receiveduring development.
app/
api/
email/
receive/route.ts # POST — Resend inbound webhook → publishes to Redis
stream/[address]/route.ts # GET — SSE stream; subscribes to Redis channel
generate/route.ts # POST — optional server-side address generation
layout.tsx
page.tsx
globals.css
components/
email-address-bar.tsx # Address display, copy, regenerate (icon-only on mobile)
burn-timer.tsx # Countdown + duration picker; compact row on mobile
inbox.tsx # Email list
email-viewer.tsx # Email content renderer
history-panel.tsx # Past addresses + clear all history
theme-toggle.tsx
theme-provider.tsx
sound-toggle.tsx # Optional new-email sound
favicon-badge.tsx # Unread count in favicon
ui/
button.tsx # Shared button component
hooks/
use-email.ts # Core state: config, emails, burn logic, history
use-sse.ts # SSE connection management
use-is-mobile.ts # Viewport ≤640px for responsive layout
use-new-email-sound.ts # Optional sound on new email
lib/
redis.ts # ioredis singleton publisher + subscriber factory
sse-manager.ts # broadcastToAddress — publishes via Redis
crypto.ts # AES-GCM encrypt / decrypt (Web Crypto)
db.ts # IndexedDB schema + CRUD via idb
domains.ts # Address generation
email-utils.ts # OTP extraction, link detection, burn progress
utils.ts # Class name utilities
Resend inbound webhook. Validates the payload and publishes the email to the Redis channel for the recipient address. All connected SSE streams subscribed to that channel receive the event.
Response:
{ "ok": true, "delivered": 1, "id": "uuid" }Opens a persistent SSE stream. Subscribes to the Redis channel poof:email:[address] and forwards any published messages to the client. Sends a heartbeat comment every 25 seconds to keep the connection alive through proxies. Unsubscribes and closes the Redis connection on client disconnect.
Event payload:
{
"type": "email",
"email": {
"id": "uuid",
"from": "sender@example.com",
"subject": "Your OTP",
"html": "<p>Your code is 123456</p>",
"text": "Your code is 123456",
"receivedAt": 1712345678901,
"attachments": []
}
}| Data | Where stored | Encrypted |
|---|---|---|
| Email content (HTML / text) | IndexedDB (browser) | Yes — AES-GCM 256-bit |
| Attachments | IndexedDB (browser) | Yes — AES-GCM 256-bit |
| Device config (address, timer) | IndexedDB (browser) | No |
| Encryption key | localStorage | No (base64 raw key) |
| Emails in transit (Redis → SSE) | Redis pub/sub (in-flight only) | No (use TLS in prod) |
| Emails at rest (server) | Nowhere | — |
The server never persists email content. Redis is used solely as a pub/sub message bus — messages are delivered to subscribers and immediately discarded.
pnpm dev # Start dev server with Turbopack
pnpm build # Production build
pnpm start # Start production server
pnpm lint # ESLint
pnpm format # Prettier
pnpm typecheck # TypeScript type checkSee CONTRIBUTING.md for development setup and pull request guidelines. This project adheres to the Contributor Covenant Code of Conduct.
MIT — see LICENSE.