It started with a recruiter email. Not the job offer kind — the "we'd love to see a full-stack project that isn't a to-do list" kind. I've been writing React components for two years, but I had nothing that showed I understood sessions, databases, rate limiting, or how to talk to external APIs without exposing secrets.
I wanted something real. Not a tutorial clone. Not a Netflix UI replica. Something that actually processes data, handles abuse, and stays secure while doing it.
I landed on disposable email because it's deceptively simple on the surface and terrifyingly complex underneath. Everyone understands the concept: click a button, get an email address, read messages, walk away. But building that? That's where the engineering starts.
Before I write another line, let me be brutally transparent: I did not build an SMTP server. I did not configure MX records, spin up Postfix, or battle spam blacklists. The underlying email infrastructure comes from the Mail.tm API (and later, mail.gw). What I built was the application layer around it — the session management, the database schema, the API abstraction, the abuse protection, and the UI that ties it all together.
If you're reading this hoping I reverse-engineered email protocols, you'll be disappointed. If you want to know how to architect a production-grade anonymous service on top of third-party infrastructure, keep reading.
I chose Next.js 15 with the App Router. Not because it's trendy, but because I wanted to understand server components, server actions, and API routes in the same mental model. TypeScript was non-negotiable — I've been burned by any types in production too many times.
Tailwind CSS + shadcn/ui for the UI. I know, I know — every new developer is using shadcn now. But here's the thing: I didn't want to spend three days writing accessible dropdowns and dialog overlays. I wanted to spend three days figuring out why my session cookies weren't persisting. shadcn gave me the primitives; I wired the logic.
Prisma + PostgreSQL for persistence. I started with SQLite because it was easy, and that decision almost destroyed the entire project later. More on that soon.
The first challenge wasn't code — it was understanding Mail.tm's API. Their documentation is sparse. You hit /domains to get available domains, POST /accounts to create a mailbox, POST /token to authenticate, and then you can read messages.
I wrote a standalone test script first. No UI, no database, just raw fetch calls to see if I could:
That last part was the most educational. I sent an email from my Gmail to the temporary address and watched the API response change from an empty array to a message object. That moment — seeing real email data flow through an API I was about to abstract — was when the project stopped feeling theoretical.
I wrapped the raw HTTP calls into a clean client: getDomains(), createAccount(), getToken(), getMessages(). The rest of the application never imports fetch directly. If Mail.tm changes their API tomorrow, I change it in one file.
This is where most tutorials would tell you to install NextAuth.js, configure OAuth providers, and call it a day. But BlinkMail doesn't have users. There are no passwords, no Google logins, no user table. The entire value proposition is zero-friction anonymity.
So how do you prove ownership of a mailbox without a user account?
I architected an opaque session system:
crypto.randomBytes)HTTP-only, Secure, SameSite=Strict cookieThe user never sees a token. The browser can't read the cookie from JavaScript. If an attacker steals the database, they only get hashes — they can't forge cookies because they don't have the raw tokens.
This was the most intellectually satisfying part of the build. I spent an entire afternoon just reading about cookie security attributes and testing session validation edge cases. It felt like building a lock from scratch and then trying to pick it.
I designed three tables:
id, token (hashed), ipAddress, userAgent, expiresAt, lastAccessedAtid, sessionId (1:1), mailtmId, mailtmToken, address, domain, localPart, expiresAtid, mailboxId, mailtmId, fromAddress, subject, intro, text, html, isRead, createdAtThe relationship chain is critical for security: Session → Mailbox → Message. When a user requests a specific message, the API doesn't just look up the message ID. It joins through the mailbox to verify the session owns it. A user with a valid session cannot read another user's messages, even if they guess the message UUID.
I also learned the hard way that Prisma's upsert is your best friend when syncing Mail.tm messages. Without deduplication via mailtmId, every inbox refresh would create duplicate rows.
Here's something most "build a temp mail" tutorials don't tell you: you are storing third-party bearer tokens in your database.
When a user creates a mailbox, Mail.tm gives you an authentication token. You need that token to fetch messages later. If you store it plaintext and your database leaks, an attacker can read every user's emails directly from Mail.tm's API.
I implemented AES-256-GCM encryption for the Mail.tm tokens. The encryption key is derived from SESSION_SECRET via SHA-256. The tokens are encrypted at rest and decrypted only when the API needs to make a request. It added maybe 20 lines of code, but the peace of mind was worth it.
Anonymous access is a magnet for abuse. Without rate limiting, someone could script mailbox creation and burn through Mail.tm's domains or exhaust your database connections.
I implemented two layers:
I also added a cleanup endpoint that deletes expired sessions and their associated mailboxes. On Vercel, this runs as a cron job at 3 AM. Locally, it runs... never, unless I remember to hit the endpoint.
The rate limiter is crude — it's an in-memory Map with timestamps. For a production product, I'd use Redis (Upstash has a great free tier). But for a portfolio project proving I understand the concept? The in-memory version is fine.
After all that backend architecture, the UI felt almost relaxing. Landing page with a single call-to-action. Inbox with copy-to-clipboard (using the native Clipboard API with a fallback for older browsers). Message list with unread indicators. A dialog for reading full message content.
I used Sonner for toast notifications instead of ugly alert() boxes. Added a refresh button with a spinning icon. Implemented auto-polling every 30 seconds so the inbox feels alive.
The most underrated UI decision: skeleton screens over spinners. When data is loading, show a gray placeholder shaped like the content. It reduces perceived load time and looks significantly more polished than a spinning circle.
This is where I almost rage-quit.
I built the entire project using SQLite because it was easy. file:./dev.db, run migrations, done. Then I tried to deploy to Vercel.
Vercel is serverless. Every request can hit a different physical server. SQLite is a file on disk. You see the problem?
I created a mailbox on one server. The user refreshed the page. The request went to a different server. That server had a different dev.db file (or none at all). 404 Mailbox not found. Every. Single. Time.
I had to migrate to Neon PostgreSQL at the 11th hour. Create a Neon project, update the Prisma provider from sqlite to postgresql, run migrations against the cloud database, update environment variables, and redeploy.
The lesson: Pick your database for your deployment target, not your development comfort. SQLite is for local prototypes. PostgreSQL is for anything that touches the internet.
I deployed. The database connected. The session system worked. I clicked "Create Temporary Email."
500 error.
The logs were heartbreaking: MailtmNetworkError: Mail.tm network error. Status 503.
Mail.tm was blocking Vercel's outbound IP addresses. Not because I did anything wrong, but because Vercel's datacenter IPs are shared by thousands of applications, and Mail.tm (understandably) blocks them to prevent abuse.
I tried switching to mail.gw, a similar service with an identical API structure. That worked... until it didn't. The reality of free temp mail APIs is that they all eventually block cloud providers.
The honest fix: I documented the limitation. The live demo showcases the architecture, UI, session flow, and database integration. For full email functionality, you run it locally where your residential IP isn't blocklisted.
Is it ideal? No. Is it a valid portfolio piece that demonstrates full-stack engineering? Absolutely. Every recruiter I've shown it to has been more interested in the session architecture and rate limiting than whether they can receive a Netflix verification code on the live URL.
localhost:3000.If you want to see the full implementation, it's open source. The README explicitly states what I built and what I didn't. I refuse to claim I built SMTP infrastructure when I clearly didn't.
The project uses:
If I revisit this project, I'd add:
But for a portfolio piece built to learn full-stack architecture? It's done. It works. It taught me more about sessions, security, and deployment than any tutorial ever could.
If you're a junior developer reading this: Don't build another to-do list. Find a service with an API, wrap it in your own architecture, handle the edge cases, and deploy it. The gaps in your knowledge only show up when you're responsible for the whole stack.
BlinkMail is live at https://blinkmail-blush.vercel.app/. Try it. Break it. Check the network tab and see how the session cookie flows. That's the real project.