Skip to content

Repository files navigation

voidmail

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.

How it works

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
Loading
  1. On first visit, the server creates a mailbox via the API, stores the mapping in Redis, and sets an httpOnly session cookie.
  2. Subsequent requests resolve the cookie to a mailbox ID and fetch messages through the API.
  3. Sessions and mailboxes share a 24-hour TTL — both expire together.
  4. 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.

Tech stack

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

Prerequisites

  • Bun (package manager and runtime)
  • Redis running locally or reachable via URL
  • A Temp Mailbox API key (API docs)

Getting started

# 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 dev

Open http://localhost:3000 — a disposable address is created automatically.

Environment variables

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.

Project structure

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)

Key implementation details

Session isolation

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.

Server functions

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.

API client

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.

Client data layer

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.

Message rendering

Email HTML is sanitized with DOMPurify before rendering (src/lib/mail/sanitize-html.ts) to prevent XSS from untrusted message content.

Blog

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.

Scripts

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 tree

Testing

Tests use Vitest with jsdom. Coverage focuses on the server-side logic and data layer:

  • src/lib/api/client.test.ts — API client request/validation
  • src/lib/session/service.test.ts — session resolve/rotate
  • src/lib/session/store.test.ts — in-memory store contract
  • src/lib/mailbox/queries.test.ts — query key helpers and cache updates
  • src/lib/mail/sanitize-html.test.ts — HTML sanitization
  • src/lib/env.test.ts — environment validation

Server dependencies can be swapped in tests via configureServerDeps / resetServerDepsForTests in src/server/deps.ts.

bun run test

Deployment

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

CI/CD

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:

  1. Checks out the repo and logs in to the GitHub Container Registry (ghcr.io).
  2. Builds a multi-arch image (linux/amd64, linux/arm64) with Buildx and layer caching.
  3. On main only — pushes the image tagged latest to ghcr.io/<owner>/<repo>.
  4. On main only — sends a GET request to the Dokploy app webhook (DOKPLOY_WEBHOOK_URL GitHub 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 start

Adding shadcn components

pnpm dlx shadcn@latest add button

Learn more

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages