Nakodo (nakodo.app) finds creators for a brand and does the emailing itself. The brand writes a brief, Nakodo writes to the creators that fit, handles the replies, and introduces the ones who say yes. One consequence of that shape runs through the whole mail layer: the brand never gets the creator's address. Until the introduction, the creator's contact details stay with us.
That single product rule decides the architecture of every outbound email, because it means replies have to come back to us, and we have to know exactly which conversation each one belongs to. The /how-it-works page is the short version for a human. This post is the mail plumbing underneath it.
Reply-To is advice, not a routing table
The naive version is to send from one shared address and set Reply-To to something that identifies the thread. It falls apart in ordinary use:
- Some mail apps and plugins drop or rewrite
Reply-To. - The creator hits reply all, so the message goes to every address in the headers, and one of them had better be meaningful.
- The creator forwards the mail to a manager, and the manager writes back from an address we have never seen, quoting our email.
So the thread identifier lives in the From address, and the Reply-To is the same address. Whatever the recipient's mail software decides to do, the address it lands on names exactly one conversation, and through that one brand and one creator.
export function threadAddress(brandName: string, token: string, domain: string): string {
return `${brandSlug(brandName)}-${token}@${domain}`;
}
The brand part is cosmetic. It exists so the address reads like a person rather than a ticketing system when it shows up in an inbox. "Café Noir & Co." becomes cafe-noir-co, and the slug function is the usual NFKD normalise, strip combining marks, collapse to [a-z0-9-], trim, cap the length, and never return an empty string. None of that is load bearing. The token decides.
A token you can read off a screen
// No 0/o, 1/l/i: the token may be read off a screen or typed.
const ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
export const TOKEN_LENGTH = 12; // 31^12, about 59 bits: not guessable
Two decisions in four lines. The alphabet drops the characters people confuse, because these tokens end up in a URL a creator might type from a phone screenshot. The length is chosen so that guessing a live thread address is not a thing you can do with a mail server and patience. randomInt from node:crypto, not Math.random, since knowing one token must not help you find the next.
Parsing an address back to a token is where the defensive work is:
const TOKEN_RE = new RegExp(`^(?:[a-z0-9-]*-)?([${ALPHABET}]{${TOKEN_LENGTH}})(?:\\+[^@]*)?$`);
export function tokenFromAddress(address: string, domain: string): string | null {
const [local, host] = address.trim().toLowerCase().split("@");
if (!local || host !== domain.toLowerCase()) return null;
return TOKEN_RE.exec(local)?.[1] ?? null;
}
The optional leading group skips the cosmetic brand prefix without caring what it is, which means a brand can rename itself and old threads still route. The trailing (?:\+[^@]*)? is there because mail systems in the middle add plus addressing on their own, and an address that comes back as acme-pqr...+SRS=xyz@ is still our thread. The host check means an address at somebody else's domain never resolves to a token, however it is shaped.
The stop address has its own shape on purpose
Every outreach email carries List-Unsubscribe with two halves, a mailto and a URL, plus List-Unsubscribe-Post for one-click. The mailto half gets a different address shape from the thread:
export function optOutAddress(token: string, domain: string): string {
return `unsubscribe+${token}@${domain}`;
}
A brand slug can never produce that shape, because the slug alphabet has no + in it. So "this is a stop request" is a property of the address, not something we infer from the body of the mail.
Then there is the ordering rule, which is the part I would have got wrong if I had not written it down:
export function outreachRecipient(addresses: string[], domain: string): OutreachRecipient | null {
let thread: string | null = null;
for (const raw of addresses) {
const address = /<([^>]+)>/.exec(raw)?.[1] ?? raw;
const optOut = optOutToken(address, domain);
if (optOut) return { kind: "opt_out", token: optOut };
thread ??= tokenFromAddress(address, domain);
}
return thread ? { kind: "thread", token: thread } : null;
}
It takes every address the mail was sent or delivered to, To and Cc and the envelope recipient, and an opt-out address anywhere in that list wins. A creator who hits reply all on a message that carries both addresses is saying stop, and a stop must never be processed as an ordinary reply that triggers a follow-up.
The brand reads the reply, not the address
Replies are shown to the brand in the app. The text goes through one more pass first:
const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
const PHONE = /(?<![\w/])\+?\(?\d[\d\s().-]{6,}\d(?![\w/])/g;
Masking email shapes is easy. Phone numbers are the hard half, because the regex that catches an international number written with spaces also catches dates, invoice numbers and view counts. Two cheap guards do most of the work: count the digits and ignore anything outside a plausible phone length, and skip anything that starts in ISO date shape. A creator writing "I can do the week of 2026-11-09, my rate is 1,200" keeps both facts intact.
This is masking in the brand's view, not security through obscurity. The address itself is never sent to the client.
An opt-out link a security scanner cannot click
Here is the bit worth stealing if you send any bulk mail at all.
Mail security products fetch the links in incoming email to check where they go. If your unsubscribe link performs the unsubscribe on GET, some of your recipients will be unsubscribed by a scanner that opened their mail before they did, and you will never find out, because from your side it looks exactly like a human clicking.
So Nakodo has two opt-out paths and they are deliberately different:
- The mail app's own unsubscribe button uses RFC 8058 one-click. That is a POST to
/api/email/opt-out?token=..., it needs no page, and it always answers 200, because showing a failure for something that was in fact recorded is worse than showing nothing. - The human link in the footer goes to
/o/<token>, which is a page that asks first and records nothing until a form is submitted.
You can see the second one right now without being a creator we wrote to, because the page is public and leaks nothing:
https://nakodo.app/o/abcdefghjkmn
That is a correctly shaped token that matches no thread, and the page says so: "That link has expired", plus a line telling the reader they can reply "stop" to the email instead. No brand name, no creator name, nothing about whether that token ever existed. With a real token the same page names the brand that wrote to you and gives you one button, and stopping covers every brand on the platform rather than just that one.
The page is noindex and force dynamic, and /o/ is in the public path allowlist, which is the other thing that bites: on a site where everything outside a short list redirects to /login, an email link to an app route is a link to a sign-in page for someone who will never have an account. I wrote about that allowlist and the Next 16 proxy.ts rename here.
For the newsletter side of the house the equivalent page is nakodo.app/unsubscribe, which uses a different token entirely, and the same expired-link treatment.
What this cost
Three small modules, one of them pure string functions with no I/O, which is why the whole address and routing layer is unit tested without a mail server anywhere near it. The pure part matters more than it sounds: address parsing is exactly the kind of code where a regression is invisible until a creator's reply silently fails to attach to a thread and a follow-up goes out as if they had said nothing.
If you want to see the product the plumbing is for, nakodo.app/how-it-works walks the flow, and the privacy page at nakodo.app/privacy is the version written for the creators on the other end of these addresses.
Top comments (0)