Disposable temporary email for instant privacy.
One address. One hour. Zero accounts.
BriefBox gives every visitor a throwaway inbox that self-destructs automatically. Incoming mail is scanned for phishing and scam signals before it appears in the UI.
| Feature | Description |
|---|---|
| No signup | Cookie-based session only — no accounts, passwords, or database users |
| 1-hour inbox | Session, address, and messages expire via Redis TTL |
| Real email | Receives real messages on your domain via Cloudflare Email Routing |
| Safety scoring | Local rule-based scanner → Safe / Warning / Dangerous badges |
| HTML + text | Parses MIME with mailparser; HTML is sanitized in the UI |
| Live inbox | Poll / revalidate to pick up new mail without manual spam-refresh |
| Copy address | One-click copy of the temporary address |
| Countdown | Live timer until the session self-destructs |
| Rate limiting | Per-IP limits on API and webhook routes |
| Monorepo | pnpm workspaces + Turborepo (web + backend) |
Emails are scored from content, links, structure, and sender patterns:
- Strong phishing / scam phrases
- Suspicious TLDs, IP links, shorteners
- Brand mismatch on credential-style links
- Urgent subject + shady link combinations
- HTML-only bait with almost no text
| Score | Level |
|---|---|
| 0–20 | Safe |
| 21–50 | Warning |
| 51+ | Dangerous |
Browser
└─ apps/web (React Router)
│
▼
apps/backend (Fastify)
│
├─ Redis (sessions + emails, TTL = session lifetime)
│
└─ POST /api/webhook/email
▲
│
Cloudflare Email Worker
▲
│
Cloudflare Email Routing (catch-all @yourdomain)
▲
Anyone sends mail to random@yourdomain
session:{sessionId} → SessionData
email-to-session:{address} → sessionId
email:{sessionId}:{emailId} → IncomingEmail
Everything is deleted automatically when TTLs expire, or immediately on session destroy.
briefbox/
├── apps/
│ ├── web/ # React Router frontend
│ └── backend/ # Fastify API
├── packages/ # shared packages (if any)
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
└── .env # root env (loaded by dotenv-cli)
Package names:
@workspace/web@workspace/backend
- Node.js 22+ (v24 Preffered)
- pnpm 11+
- Redis (local or hosted, e.g. Upstash)
- A domain on Cloudflare (for real inbound email)
- Optional: Docker
Create a root .env copying .env.example:
NODE_ENV=development
VITE_ENV=development
BACKEND_PORT=4000
WEB_URL=http://localhost:5170
API_BASE_URL=http://localhost:4000
SESSION_COOKIE_NAME=briefbox_session
SESSION_TTL=1h # supports: 30m, 1h, 2h, 1d, etc.
REDIS_URL=rediss://default:xxxxxxxx@yyyy
EMAIL_DOMAIN=briefbox.dev
WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxx| Variable | Used by | Notes |
|---|---|---|
API_BASE_URL |
Web loaders / server | Use host.docker.internal or Compose service name in Docker |
WEB_URL |
Backend CORS | Must match the site origin |
EMAIL_DOMAIN |
Backend | Domain used in generated addresses |
WEBHOOK_SECRET |
Backend + CF Worker | Shared secret header |
pnpm installLocal example:
docker run --rm -p 6379:6379 redis:7-alpineOr point REDIS_URL at a hosted instance.
From repo root:
# both
pnpm dev
# or separately
pnpm web:dev
pnpm backend:devTypical local URLs:
- Web:
http://localhost:5170 - API:
http://localhost:4000
pnpm web:build
pnpm web:start
pnpm backend:build
pnpm backend:startBackend production build rewrites path aliases (~/...) and ESM .js extensions before node dist/index.js.
BriefBox receives real mail by routing all addresses on your domain through Cloudflare into a Worker, which POSTs to your backend webhook.
- Add the domain in the Cloudflare dashboard.
- At your registrar (e.g. Namecheap), set Cloudflare nameservers.
- Wait until the domain status is Active.
DNS management moves to Cloudflare after nameserver update.
- Open Email → Email Routing → your domain.
- Enable Email Routing and allow Cloudflare to add MX / TXT records.
- Add and verify at least one destination address (required once by Cloudflare).
- Go to Routing rules / Routes.
- Enable Catch-all address.
- For the first test you may Send to an email.
- Later switch the action to Send to a Worker (below).
Catch-all is required because BriefBox addresses are random (x7k9p2m@yourdomain.com).
- Workers & Pages → Create Worker.
- Name it e.g.
briefbox-email-worker. - Deploy, then paste a worker that forwards to your API:
export default {
async email(message, env, ctx) {
try {
const from = message.from;
const to = message.to;
const subject = message.headers.get("subject") || "(no subject)";
// Read the raw email content
const rawEmail = await new Response(message.raw).text();
const payload = {
from,
to,
subject,
raw: rawEmail, // full raw email (headers + body)
headers: Object.fromEntries(message.headers),
receivedAt: new Date().toISOString(),
};
const webhookUrl = env.WEBHOOK_URL; // e.g. https://api.yourdomain.com/api/webhook/email
const secret = env.WEBHOOK_SECRET;
if (!webhookUrl) {
console.error("WEBHOOK_URL is not set");
message.setReject("Webhook not configured");
return;
}
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(secret ? { "X-Webhook-Secret": secret } : {}),
},
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error("Webhook failed:", response.status, await response.text());
// You can choose to reject or just log
// message.setReject("Failed to process email");
}
} catch (err) {
console.error("Email Worker error:", err);
// message.setReject("Internal error");
}
},
};- In the Worker Settings → Variables, set:
| Variable | Example |
|---|---|
WEBHOOK_URL |
https://api.yourdomain.com/api/webhook/email |
WEBHOOK_SECRET |
same value as backend WEBHOOK_SECRET |
For local testing, expose the backend with a tunnel (Cloudflare Tunnel, ngrok, etc.) and put that URL in WEBHOOK_URL.
- Email Routing → Catch-all
- Action: Send to a Worker
- Select
briefbox-email-worker - Save
EMAIL_DOMAIN=yourdomain.comGenerated addresses become:
{random}@yourdomain.com
- Open BriefBox and copy the temp address.
- Send mail from Gmail to that address.
- Confirm Worker logs show a delivery.
- Confirm backend logs show
POST /api/webhook/email. - Refresh / wait for revalidate — message appears with a risk badge.
Build from the monorepo root so workspace packages resolve.
docker build -f apps/web/Dockerfile -t briefbox-web .
docker run --rm -p 3000:3000 --env-file .env briefbox-webdocker build -f apps/backend/Dockerfile -t briefbox-backend .
docker run --rm -p 4000:4000 --env-file .env briefbox-backendInside a container, localhost is the container itself.
For SSR/loaders calling the API:
# server-side and browser
API_BASE_URL=http://localhost:4000Or use Docker Compose service DNS (http://backend:4000).
Ensure the backend listens on 0.0.0.0, not only 127.0.0.1.
-
NODE_ENV=production - Strong
WEBHOOK_SECRET - Redis with persistence appropriate for your host
-
EMAIL_DOMAINmatches Cloudflare-routed domain -
WEB_URL/ CORS origins set to real frontend URL - Cookie
Securein production - Rate limits enabled
- Health check monitored (
/api/health) - Worker
WEBHOOK_URLpoints at public API - MX records active for Email Routing
- Sessions are anonymous and short-lived by design.
- Safety scoring is heuristic, not antivirus.
- Sanitize HTML before rendering (
DOMPurify). - Webhook is protected by shared secret header.
- Do not store attachment bytes in Redis for MVP (metadata-only is optional).
| Layer | Stack |
|---|---|
| Monorepo | pnpm workspaces + Turborepo |
| Frontend | React Router (framework mode) |
| Backend | Fastify |
| Storage | Redis (ioredis) |
| Mail parse | mailparser |
| Inbound | Cloudflare Email Routing + Worker |
| Language | TypeScript |
MIT
BriefBox is a full temporary-email loop:
- Visit the site → get a disposable address
- Receive real mail on your domain
- Scan → store in Redis → show in inbox
- After the timer, everything disappears
No accounts. No permanent data. Built for privacy testing and throwaway signups.

