A disposable email inbox — open the site, copy an address, receive mail, and walk away. No accounts, no passwords, no profile to maintain.
Live site: voidmail.live
Built with TanStack Start (React + SSR + server functions), backed by the Temp Mailbox API for actual mail delivery and Redis for per-visitor session isolation.
Each visitor gets a private inbox tied to an opaque session cookie. The browser never sees the API key; all mailbox operations run on the server.
flowchart LR
Browser["Browser\n(TanStack Query, 30s poll)"]
Server["TanStack Start\n(server functions)"]
Redis["Redis\nsession → mailboxId"]
API["Temp Mailbox API\n(bux.vybz.online)"]
Browser -->|"httpOnly cookie"| Server
Server --> Redis
Server -->|"X-API-Key"| API
- On first visit, the server creates a mailbox via the API, stores the mapping in Redis, and sets an
httpOnlysession cookie. - Subsequent requests resolve the cookie to a mailbox ID and fetch messages through the API.
- Sessions and mailboxes share a 24-hour TTL — both expire together.
- The client polls for new messages every 30 seconds via TanStack Query.
For a deeper walkthrough of the design decisions, see the blog post in content/blog/building-voidmail-with-temp-mail-api.mdx.
| Layer | Choice |
|---|---|
| Framework | TanStack Start (React 19, Vite, SSR) |
| Routing | TanStack Router (file-based routes in src/routes/) |
| Data fetching | TanStack Query + server functions |
| Session storage | Redis (ioredis) |
| Mail backend | Temp Mailbox API |
| UI | Tailwind CSS 4, shadcn/ui (Radix) |
| Validation | Zod |
| HTML sanitization | DOMPurify (isomorphic-dompurify) |
| Runtime / deploy | Bun, Nitro, Docker |
- Bun (package manager and runtime)
- Redis running locally or reachable via URL
- A Temp Mailbox API key (API docs)
# Install dependencies
bun install
# Copy and fill in environment variables
cp .env.example .env
# Start Redis (example with Docker)
docker run -d --name voidmail-redis -p 6379:6379 redis:7-alpine
# Run the dev server on port 3000
bun run devOpen http://localhost:3000 — a disposable address is created automatically.
| Variable | Required | Default | Description |
|---|---|---|---|
VOIDMAIL_API_BASE_URL |
No | https://bux.vybz.online/v1 |
Temp Mailbox API base URL |
VOIDMAIL_API_KEY |
Yes | — | API key sent as X-API-Key on every backend request |
VOIDMAIL_SITE_URL |
No | request host | Public site URL for sitemap and canonical links |
REDIS_URL |
No | redis://localhost:6379 |
Redis connection string |
SESSION_TTL_SECONDS |
No | 86400 |
Session and mailbox lifespan in seconds (24 h) |
Server env is validated at startup via Zod in src/lib/env.ts. The API key is never exposed to the client bundle.
src/
├── routes/ # File-based routes
│ ├── index.tsx # Inbox (home)
│ ├── message/$messageId.tsx
│ ├── blog/ # MDX blog posts
│ └── sitemap[.]xml.tsx
├── server/
│ ├── mailbox.ts # Server functions (session, messages)
│ └── deps.ts # Singleton API client + session service
├── lib/
│ ├── api/ # Typed MailboxApiClient + Zod schemas
│ ├── session/ # SessionService, Redis store, cookie constants
│ ├── mailbox/ # TanStack Query keys, options, cache helpers
│ └── mail/ # HTML sanitization for message bodies
├── hooks/use-mailbox.ts # Client hook composing session + message queries
└── components/ # Mailbox UI, layout, shadcn/ui primitives
content/blog/ # MDX blog posts (eager-loaded via import.meta.glob)
SessionService (src/lib/session/service.ts) maps each browser session to one mailbox:
resolve— returns the existing session or creates a new mailbox + Redis entry.rotate— deletes the old mailbox, creates a fresh one, keeps the same session ID (used by "New address").
Session data (sessionId, mailboxId, email, timestamps) is stored in Redis under voidmail:session:{id} with a TTL matching SESSION_TTL_SECONDS.
All mailbox operations live in src/server/mailbox.ts as TanStack Start server functions:
| Function | Method | Purpose |
|---|---|---|
resolveMailboxSession |
GET | Resolve or create session; set cookie if new |
rotateMailboxSession |
POST | Swap to a new address |
listMailboxMessages |
GET | List inbox messages for the current session |
getMailboxMessage |
GET | Fetch a single message by ID |
clearMailboxMessages |
POST | Delete all messages in the inbox |
The session cookie is httpOnly, sameSite: lax, and secure in production.
MailboxApiClient (src/lib/api/client.ts) wraps the Temp Mailbox API with Zod-validated responses. Endpoints cover domains, mailboxes, and messages. Errors are normalized through ApiError.
TanStack Query drives the UI:
- Session query — fetched once per page load, never stale.
- Messages query — refetches every 30 s (
MAILBOX_REFETCH_INTERVAL_MS). - Route loader on
/prefetches both for faster first paint.
The useMailbox hook in src/hooks/use-mailbox.ts composes these queries with rotate/clear mutations.
Email HTML is sanitized with DOMPurify before rendering (src/lib/mail/sanitize-html.ts) to prevent XSS from untrusted message content.
Posts are MDX files in content/blog/. Each exports a meta object (title, date, cover, excerpt). src/lib/blog.ts loads them at build time and exposes helpers for the blog index and slug routes.
bun run dev # Dev server (port 3000)
bun run build # Production build (Nitro + Vite)
bun run start # Run built server (.output/server/index.mjs)
bun run test # Vitest unit tests
bun run lint # ESLint
bun run format # Prettier + ESLint fix
bun run check # Prettier check
bun run generate-routes # Regenerate TanStack Router route treeTests use Vitest with jsdom. Coverage focuses on the server-side logic and data layer:
src/lib/api/client.test.ts— API client request/validationsrc/lib/session/service.test.ts— session resolve/rotatesrc/lib/session/store.test.ts— in-memory store contractsrc/lib/mailbox/queries.test.ts— query key helpers and cache updatessrc/lib/mail/sanitize-html.test.ts— HTML sanitizationsrc/lib/env.test.ts— environment validation
Server dependencies can be swapped in tests via configureServerDeps / resetServerDepsForTests in src/server/deps.ts.
bun run testThe app ships as a multi-stage Docker image (Bun) and is deployed with Dokploy — a self-hosted PaaS for containerized apps. GitHub Actions builds the image and notifies Dokploy when a new release is ready.
Workflow: .github/workflows/deploy-ci.yml
| Trigger | What runs |
|---|---|
Push to main |
Build, push image to ghcr.io, call Dokploy webhook |
Pull request to main |
Build only (validates the Dockerfile; no push) |
Manual (workflow_dispatch) |
Same as above, depending on branch |
On each run the workflow:
- Checks out the repo and logs in to the GitHub Container Registry (
ghcr.io). - Builds a multi-arch image (
linux/amd64,linux/arm64) with Buildx and layer caching. - On
mainonly — pushes the image taggedlatesttoghcr.io/<owner>/<repo>. - On
mainonly — sends aGETrequest to the Dokploy app webhook (DOKPLOY_WEBHOOK_URLGitHub secret). Dokploy pulls the latest image and redeploys the running container.
Configure the webhook URL in your Dokploy application settings, then add it as a repository secret in GitHub. PR builds skip the push and webhook steps so forks and feature branches cannot trigger a production deploy.
Production requires the environment variables above plus a reachable Redis instance. The container exposes port 3000 and runs bun run start.
# Local production preview
bun run build
bun run startpnpm dlx shadcn@latest add button- TanStack Start docs
- TanStack Router docs
- Temp Mailbox API (Swagger)
- Dokploy — deployment platform used for production