Compass Navigation Streamline Icon: https://streamlinehq.com
applied cartography

Subpaths vs. subdomains

If you want to have user-level namespaces on a single domain — such as company.com/justin — you have two options: namespacing via subpath (company.com/justin) and namespacing via subdomain (justin.company.com).

When I started Buttondown back in 2018, I went with the former. This ended up being a fairly important technical decision that I did not give the weight it probably deserved, as is often the case with many technical decisions I made in 2018, Buttondown or otherwise. Many such cases, as they say.

To the best of my recollection, it was for three reasons:

  1. Subpath-based routing was how TinyLetter did it, and I was aggressively pattern-matching onto them.
  2. It's hard to believe now, but wildcard SSL certificates — and SSL writ large — was much more of a wild west. The idea of having to figure that out under the auspices of Heroku's ergonomic-but-walled-garden approach to domains seemed like a very steep hill to climb.
  3. It was just frankly easy to think about subpaths in the context of the technical product itself, because that's how most places did things.

There were good and bad consequences of this decision, many of which I only came to appreciate through the passage of time.

The good:

  1. It was and still is fairly easy to handle routing within the core app itself, once you know that you are in the app itself. (More about this later.)
  2. I think on the whole most non-technical users understand a subpath slightly more than they understand a subdomain, because it gloms onto concepts they're more familiar with — more people who use Buttondown have used Twitter than Tumblr.

That's the full list of good things. It was easy to build, and users slightly, marginally prefer it.


Sadly, the list of bad things is gnarlier.

  1. Buttondown offers custom domains, which means we also have to flex between two types of routing: subpath-based and subdomain-based. For every route we expose as part of the archives, we need to be able to route it within the context of our core domain and within the context of a subdomain. This is the kind of complexity that compounds silently — every new feature or endpoint is twice as much routing work, twice as many edge cases to test.
  2. We increase our SEO liability. When a random British gambling SEO grey hat signs up for us and evades our KYC filters, it negatively impacts us and every single user, not just them. With subdomain routing, the blast radius of a spammer's SEO toxicity is contained to their subdomain; with subpath routing, it poisons the entire domain.
  3. We now have to actively think about how namespacing works within our system. I wrote about this in haproxy, but this basically means that our marketing site has to be vividly aware of any usernames in our application side and vice versa. This is quite annoying — you can't just add a /pricing page without first checking that nobody has registered pricing as a username, and you need a growing denylist that you'll inevitably forget to update.
  4. Rate limiting and abuse mitigation become harder. With subdomains, you can apply rate limits, WAF rules, and abuse detection at the subdomain level; with subpaths, everything hits the same origin, and distinguishing between a misbehaving user's traffic and legitimate traffic to your marketing site requires more surgical instrumentation.
  5. You lose the ability to isolate user-generated content at the browser level. Subdomains get treated as separate origins by browsers, which means cookies, localStorage, and other client-side state are naturally sandboxed. With subpaths, a malicious user's archive page shares an origin with your marketing site, your login page, and every other user's archive. This is not a theoretical concern — it's an XSS blast radius problem.
  6. Analytics and observability get muddier. Want to know how much traffic your marketing site gets versus your users' archives? With subdomains, your monitoring tools can trivially separate these. With subpaths, you're parsing URL patterns and maintaining filter rules that break every time you add a new route.

I don't think either option is strictly better than the other. Certainly, I regret — with the benefit of hindsight — not going with subdomain routing. At the same time, it's also something that is not so inordinately painful that I've dropped everything and migrated.

But if you're on the fence between the two, I would recommend subdomain-based routing, especially if you're planning on offering custom domain functionality. Because 90% of the work you need to do for subdomain routing will overlap with custom domains anyway — wildcard DNS, per-tenant TLS, subdomain-aware request routing — and you'll have a cleaner, more isolated architecture from day one.


About the Author

I'm Justin Duke — a software engineer, writer, and founder. I currently work as the CEO of Buttondown, the best way to start and grow your newsletter, and as a partner at Third South Capital.

Colophon

You can view a markdown version of this post here.