DEV Community

China Wang
China Wang

Posted on Edited on Originally published at minglist.hashnode.dev

I Built a Classifieds Site Where You Never Give Out Your Email — Here's the Architecture

Update (Oct 27, 2026): MingList is now live on Product Hunt! You can try the relay yourself at minglist.top — and if you're feeling generous today, we're on the Product Hunt front page collecting feedback and votes.

Full build log: minglist.hashnode.dev

Every time you post a classified ad — a used phone, a sublet, a gig offer — you're asked to hand over your email address. That address then sits on a public page, scraped by bots, and three days later your inbox fills up with "You've won a $500 gift card" and "URGENT: verify your account now."
I kept running into this problem, so I built the thing I wished existed: MingList — a free classifieds site where the posting form doesn't even have an email field. It just launched. Here's how it works under the hood.
The core idea: a relay address per listing
Instead of collecting your email, MingList generates a per-listing relay address, like seller-8f3k@relay.minglist.top. Buyers see only that address on the listing page. When they reply, the relay forwards the message to your real inbox — with your real address stripped out. You reply from your normal mail client, and the response goes back through the relay, so the buyer still never learns who you are.
Two-way, per-listing, disposable by design. If a listing attracts spam, the relay can be muted without touching anything else.
The stack (and why it costs $0/month)
I'm a solo developer, so the stack had to be boring and cheap:

  • Cloudflare Pages hosts the frontend — plain HTML/JS, no framework tax, cached at the edge.
  • Cloudflare Workers run the API: posting, replying, relay routing. Workers sit naturally in the mail path, which makes the relay logic a simple function instead of a separate email microservice.
  • Cloudflare D1 (SQLite at the edge) stores listings and relay mappings. One logical database, replicated close to users.
  • Resend handles the actual email delivery for relay forwards. Everything runs inside the Cloudflare free tier. The only variable cost is email volume through Resend, which is negligible at classifieds scale. The whole site has no accounts, no passwords, and no card — posting asks for a display name and a listing, nothing else. Design decisions worth sharing
  • No accounts at all. Sign-up friction kills classifieds sites, and stored accounts create a data-leak liability. Removing the account system removed an entire class of privacy problems. The tradeoff is lighter moderation hooks — something I'm actively working on (report buttons and rate limiting are next on the roadmap).
  • The relay is per-listing, not per-user. A per-user alias would leak correlation across listings ("this seller on three different posts is the same person"). Per-listing aliases keep each ad an independent identity.
  • Bot-resistant by construction. Since there's no email address on the posting page to scrape, the classic "harvest ads → spam sellers" pipeline has nothing to harvest. Buyers write to the relay; the relay decides what gets through.
  • Trust without identity. There's a trust & safety page explaining exactly what is and isn't collected — because a privacy product that can't articulate its own data flow isn't credible. What I learned building it
  • Email is an API. Treating replies as structured messages routed through Workers (rather than a mailbox to check) made the whole feature surprisingly small.
  • Removing features is a feature. "No email field" became the entire marketing hook — the demo writes itself, and people instantly get it.
  • Free-tier serverless is enough for launch. The launch-week plan is Product Hunt, Reddit, Show HN — none of that traffic requires a provisioned server. Try it The site is live at minglist.top — free, no account, no card. There's also a 1-minute explainer video showing the relay flow. If you've built something on Workers/D1, I'd love to hear how you handled email — the relay edge cases are the most interesting part of this system. Ask me anything in the comments.

Full build log: minglist.hashnode.dev

Top comments (12)

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

The relay-kill-on-close rule is the part I'd copy — closing a listing killing the relay in both directions covers the 'sold it months ago' case cleanly. The edge I'd watch is the in-flight reply: a buyer hits reply just as the listing closes, and the relay is already dead, so their message bounces instead of reaching a seller who might still want it. Do you hold a short grace window where the relay still delivers replies to messages started before the close, or is the kill immediate? And on expiry-with-extend: when an owner extends, does the listing keep the same relay address, or mint a new one — if it's a new one, the old address becomes a dead end that anyone still holding it will bounce against?

Collapse
 
no1guangming profile image
China Wang •

Great catch on both — and answering honestly made me audit the code, which
found a real gap.

The in-flight one: closing a listing kills web replies instantly (the reply
endpoint requires status=active), and I thought the relay matched — but
checking tonight, the mail worker only drops on status=removed, not closed.
So a reply+token email to a closed listing would still forward. That's the
opposite of the edge you flagged: not "bounced when the seller still wanted
it" but "delivered after the seller already closed". Fixed as of tonight:
closing a listing now deactivates its relay the same way removal does, and
reopening reactivates it. Kill is immediate by design — if the ad says
closed, the page says closed, and the mailbox should agree.

On extend: the relay is minted once per listing and bound to the post id.
Renewal and admin restore flip the same row's is_active back on — same
address, no re-mint, nothing holding the old address hits a dead end. What
expires is the listing: fixed 30-day TTL with a 7-day grace before the cron
removes it. Your "expiry-with-extend" framing is where this lands — nudge
the owner to confirm instead of a silent TTL, and the per-category tuning
problem (bike vs apartment) becomes a nudge-cadence problem instead.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Following up on the relay thread — I’m building rootmail around this kind of plumbing: per-message delivery state, bounce/complaint handling, and suppression lists for SES senders. It’s in invite-only beta: rootmail.io/beta — happy for you to try it if useful, and happy to keep the thread technical either way.

Thread Thread
 
no1guangming profile image
China Wang •

Appreciate the invite — and yes, I'll take it. Bounce and
complaint handling is exactly the part of relay plumbing I
haven't built yet: today a bounced forward just dies quietly
in my logs, and a relay that keeps forwarding to someone who
complained is worse than one that stops. Per-message delivery
state would fix both.

Curious how it maps onto a relay where the "sender" is an
address that only exists because of a single listing — happy
to share what I find. Either way, thanks for keeping this
thread sharp; your questions made the code better this week.

Thread Thread
 
no1guangming profile image
China Wang •

Quick follow-up — the beta page is asking for an invite
code. Mind sending one over?

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

I'll send one over outside this thread.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

A complaint should shut that relay down for that recipient right away, while a bounce is just one message failing. Treating both the same way is where the quiet log death starts.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Small correction to my last message: you don't need to wait on a code from me. Go to rootmail.io/beta and register with your email — the invite code comes through that signup process automatically. Once you've done that you're in the signup flow.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

that closed-but-still-forwarding gap is a nasty one to catch. the nudge instead of a silent TTL is the better trade.

Thread Thread
 
no1guangming profile image
China Wang •

Thanks! Credit where it's due — your question is what made
me go read the mail path line by line. The nudge model feels
right: silent TTLs trade a tiny bit of storage for trust,
and trust is the whole product here.

Collapse
 
wbcomdesigns profile image
Wbcom Designs 💡 •

Relay addresses are a smart default. On the directory sites we work on, the next thing after spam is stale or fake listings, so an expiry date and a "report this" button pay off early. (We build directory software at Wbcom Designs, so this one's close to home.)

Collapse
 
no1guangming profile image
China Wang •

Appreciated — and stale listings are the quieter problem nobody warns you about.

Reporting is already there (button on the listing page, hitting an endpoint with
its own rate limit), but two people asked me for it in the same week, which tells
me it isn't visible enough at the moment someone gets suspicious. That's a
placement fix, not a feature.

Expiry is the harder one. Closing a listing already kills its relay in both
directions, which covers the "sold it months ago" case, but there's no clock on
it yet. The tuning problem: an expiry that suits a bike is wrong for an
apartment. My lean is expiry-with-extend.

How do you handle it in directory software — fixed TTL, or nudging the owner to
confirm it's still live?