Skip to content

grtsnx/poof

Poof

Poof logo

Your email, but it burns.

Next.js TypeScript Tailwind CSS Privacy-first Zero server storage License MIT


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.

Features

  • 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

Tech Stack

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

How It Works

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

Getting Started

Prerequisites

  • Node.js 18+
  • A Redis instance (local, Upstash, Railway, etc.)
  • A Resend account with a verified domain and inbound email enabled

1. Clone and install

git clone https://github.com/yourusername/poof.git
cd poof
pnpm install

2. Configure environment

cp .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

3. Start Redis

For local development:

# macOS
brew install redis && brew services start redis

# Docker
docker run -p 6379:6379 redis:alpine

For production, use a hosted Redis service such as Upstash (free tier available) and set REDIS_URL to the connection string they provide.

4. Configure Resend inbound

  1. Go to Resend → Domains → your domain → Inbound
  2. Add the MX record Resend provides
  3. Set the webhook URL to https://yourapp.com/api/email/receive
  4. Optionally set a webhook secret and add it to WEBHOOK_SECRET

5. Run locally

pnpm dev

Open http://localhost:3000.

Local inbound emails: Resend can't reach localhost. Use a tunnel like ngrok and set NEXT_PUBLIC_APP_URL to your tunnel URL; point the Resend webhook at https://your-ngrok-url.ngrok-free.app/api/email/receive during development.

Project Structure

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

API

POST /api/email/receive

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" }

GET /api/email/stream/[address]

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": []
  }
}

Privacy Model

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.

Scripts

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 check

Contributing

See CONTRIBUTING.md for development setup and pull request guidelines. This project adheres to the Contributor Covenant Code of Conduct.

License

MIT — see LICENSE.

About

Spam-proof with zero regrets. Encrypted locally. Self-destructs on command like your last relationship, but on purpose.

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors