Weeknotes from Buttondown logo

Weeknotes from Buttondown

Subscribe
Archives
September 28, 2025

Unshipping things

Buttondown has been around long enough, and the core set of assumptions around which we've laid our foundations have shifted and mutated so much, Ship of Theseus style, that sometimes we get to wake up one day and realize we can pull out an entire chunk of annoying logic without actually causing any issues downstream.

Here's one example: Stripe launched Payment Links a few years ago as basically a no-code way to spin out checkout sessions. This was kind of perfect for us because we had just launched paid subscriptions and had built out some of the logic for the base case, but nothing else (upsells; currency conversion; etc.) and we were able to just automatically spin up payment links on behalf of our users instead of having to really try and dig in and handle all of the edge cases and persistence.

That worked for a while, but over time, the number of things we needed to do in-house grew. We had a handful of customers who wanted esoteric tax ID setups; we had a number of customers who wanted to do pay-what-you-want and metered billing and things that payment links didn't support. The list grew and grew and grew, and we finally got to a point where we basically had a CheckoutSession creation flow that was at parity and in fact exceeded the functionality of what you get out of the box from Payment Links themselves. At the same time, we were still stuck with an orchestration layer to sync Payment Links, and these two things could get subtly out of sync with one another. For instance, if someone were to create a payment link in the Stripe dashboard or remove some existing metadata from one of the ones we created, this caused noise (our invariants have suddenly become variant!) and angst.

And Ben finally suggested, "Hey, can we just get rid of payment links?" The answer was, resoundingly and satisfyingly, yes. And we got to embark on a sequence of PRs that are so rare: pure deletions with no actual change to functionality and a cleaner state machine as a result.


Here’s another: hidden settings. It’s exceedingly rare that we add new hidden settings right now, but it was a (foolishly) common hammer that I reached for a few years ago before I was a bit more confident in two areas of our product strategy (integrations and the subscription flow, both of which as you might imagine are prone to idiosyncratic requests.)

We are embarking (as mentioned last week in 2.0; DjangoCon; Investments vs. bets) in a bit of a UX bash on subscription flows, and by far the most satisfying part of the entire enterprise has been the following:

1. Oh, this is janky because we have this code path for a hidden setting.
2. Is that actually being used by anyone still? The last new user we flagged into it was in 2022.
3. No? Sick, [sounds of smashing the delete key]


It's really, really easy to get trapped in the forest of incrementalism. I love knowing that we end every day with a product that is 0.2% better than it started, but in so doing, I often forget to take a step back and re-examine some of the core assumptions that were valid and virtuous two or three years ago, but simply make no sense through today's lens.

This is also — zooming out a bit — where spending a lot of time with a single project, regardless of your definition of the word “project”, yields outsized value. A lot of the lessons I’ve learned in this arena I think are impossible to learn without the passage of time slowly and subtly mutating the world.

Don't miss what's next. Subscribe to Weeknotes from Buttondown:
X
Powered by Buttondown, the easiest way to start and grow your newsletter.