Applied Cartography https://jmduke.com Essays and media reviews by Justin Duke en-us Sun, 26 Apr 2026 00:22:37 GMT https://jmduke.com/favicon.svg Applied Cartography https://jmduke.com What's Up, Doc? https://jmduke.com/posts/whats-up-doc.html https://jmduke.com/posts/whats-up-doc.html Fri, 24 Apr 2026 00:00:00 GMT What's Up, Doc? is, I guess, just a perfect film. What's Up, Doc? is, I guess, just a perfect film.

I can remember exactly one other movie of its ilk that I watched with sheer glee — amazed by how contemporaneously funny it was, by how awful it was, and by how obviously, in retrospect, it influenced so much of the genre: the-thin-man. But even more so than that film, What's Up, Doc? is all gas, no brakes. The commitment to screwball never wavers, not even for a single second, ramping up and up and up in abject silliness until — as Babs says in a memorable closing line — you simply surrender to its tidal wave.


Here's a confession I'll offer in lieu of anything interesting to say about this terrific, hilarious film that I recommend wholeheartedly: I don't think I've actually ever seen anything with Barbra Streisand in it before. In one of those self-reflexive memes, I know her more for the Streisand effect — literally the name — than any specific work of art. Until now.

And she is so completely winning in this, in a way that I don't think I've actually seen from any other lead actress. It is rare for Hollywood to let a lead actress be funny, horny, and charming all at once. The industry, if it deigns to let women be sexual and possessed of a sense of humor, usually consigns them to the realm of the character role, or tries to diffuse things with some other means — i.e. fat jokes. But Babs here, who is in many ways the original manic pixie dream girl (albeit perhaps more of a nightmare), is an absolute tornado. I'm not sure I would find her as charming as her male retinue does, diegetically, but she commands every scene she's in and demands your attention, never letting pesky things like pathos or logic get in the way of her Looney Tunes sensibilities.

Just an absolute delight.

]]>
Slay the Spire 2 https://jmduke.com/posts/slay-the-spire-2.html https://jmduke.com/posts/slay-the-spire-2.html Mon, 20 Apr 2026 00:00:00 GMT I have played Slay the Spire 2 for around thirty hours. I am, after writing this review, uninstalling it from my laptop and praying my resolve is strong enough to keep it that way: a testament, to put it mildly, to how perfectly calibrated this game is and how effortlessly it enables my least productive tendencies. I have played Slay the Spire 2 for around thirty hours. I am, after writing this review, uninstalling it from my laptop and praying my resolve is strong enough to keep it that way: a testament, to put it mildly, to how perfectly calibrated this game is and how effortlessly it enables my least productive tendencies.

This is as good an opportunity as any to talk more about Slay the Spire itself, because — as I inelegantly put it to my brother — the sequel is a lazy one. By which I don't mean any moral judgment, but simply that it is a pure continuation of its predecessor: same engine, same mechanics, same gameplay loop, a host of new cards, new characters, new events, new enemies.

To say that Slay the Spire 2 is derivative is both true and laudatory. I played the original game for at least five hundred hours and could easily pour the same amount into this one. I find Slay the Spire to be such a beautiful and addicting game because it seems expressly designed to strip away everything from an RPG that is repetitive or meaningless. Every decision you make is meaningful. Randomization between runs keeps everything fresh. Emergent interactions between the game's various mechanics provide a sense of progression and mastery, at both the implicit and explicit levels. Each run takes forty-five to sixty minutes, which is not just perfect for a single sitting but just long enough to justify booting up a new one. Every single time I've told myself I would stop in the middle of a Slay the Spire run, I have failed against the gravitational pull of one more turn, one more event, one more act.

Many games have followed in its wake, but few come even close to the original formula, which is so beautifully forged, design iteration by design iteration, to be both simple and fractally complex.


The sequel adds a couple of new characters and mechanics, and these are interesting — they play around with converting the core resources of the game. The Regent has not one but two sources of energy. The Necrobinder's block, rather than being transient and disappearing at the end of each round, is persistent, manifesting as an undead hand you have summoned. Scaling in general comes less from powers (static, boring) and more from emergent factors like exhaust. Some mechanics are underbaked, like Quests — that's okay, this is technically Early Access.

If I were being more intellectually honest, I'd evaluate Slay the Spire 2 the same way I might evaluate Ocean's Twelve: a derivative but pleasurable piece of art who owes much of its success to its predecessor.

But the difference between the two is this: I know I have revisited and will revisit Ocean's Eleven — the superior film — many, many times over. I'll probably not rewatch Ocean's Twelve ever again, even though I enjoyed it. And I don't see myself playing Slay the Spire (that is, the original) again, even separate from its opioid-esque tendencies. Part of this is, perhaps, exhaustion — the extent to which I feel I've completely finished the original; but mostly the reason is because Slay the Spire 2 feels like a major version of a sort. Even the characters who have carried through from the first into the second are more nuanced and interesting to play this go-round/ They improved the formula and in doing so obsoleted the old one, which is a very hard thing to do.

]]>
Four years in the maelstrom https://jmduke.com/posts/the-maelstrom.html https://jmduke.com/posts/the-maelstrom.html Sun, 19 Apr 2026 00:00:00 GMT A baby holds your hands, and then suddenly, there's this huge man lifting you off the ground, and then he's gone. Where's that son?

A baby holds your hands, and then suddenly, there's this huge man lifting you off the ground, and then he's gone. Where's that son?

And at one point, I noticed that Grotowski was at the center of one group huddled around a bunch of candles that they'd gathered together. And like a little child fascinated by fire, I saw that he had his hand right in the flame and was holding it there! And as I approached his group, I wondered if I could do it. I put my left hand in the flame, and I found I could it there for as long as I like and there was no burn and no pain. But when I tried to put my right hand in the flame, I couldn't hold it there for a second. So, Grotowski said, 'If it burns, try to change some little thing in yourself.' And I tried to do that - didn't work.


This time two years ago, I wrote two-years sitting in an airport lobby, waiting to head to MicroConf. I was unable to make it to MicroConf this year, but the absence reminded me about that post — which, to this day, is one of my favorites that I've ever read or written. I wanted to think about what has changed over those past two years.

...which is what I said last week. In truth, I've tried and failed a few times to sit down and write this essay. My most successful writing comes from when I have a breadcrumb into a maze that I can follow to its logical conclusion; introspection of this particular vintage does not lend itself neatly to single tracks.

So instead, like that preceding essay, I will eschew any sort of flow and just try to touch on the salient points in listicle form.

  1. Two years ago, I struggled with the idea of referring to myself as a founder. Now, I find myself struggling with the inverse — it is surreal, a word I am using with increasing frequency, that Buttondown has a larger profile than I do, and rather than its identity being subsumed into mine, I feel the opposite happening. It is gratifying and somewhat disorienting to have the majority of your users not know that you exist; it is gratifying and disorienting to have a majority of your users on a first-name basis with someone at the company who is not you.

  2. It is hard to pretend otherwise, as many other people do, how thoroughly fatherhood has permeated my role as a founder (and, thankfully, not vice versa). Jason Cohen writes a good essay about only being able to do two big things well, and this has been my exact experience. I am deeply proud of the fact that I can say, without reservation, that I spend even more time with Lucy and Haley than I would in a more conventional job. I can also say, earnestly, that the business has probably not grown as fast as it could if that weren't the case — and I'm happy to have a life where I have the agency to make that choice. Both running a company and being a father are infinite vessels. There is no point in the day in which you can truthfully say to yourself, "Job's done, there's nothing left to do." One of the things that has been hardest for me — and you can see it in the months in which I publish rarely — is to let all of my oxygen be swallowed up by those two things alone, which are wondrous things but in of themselves insufficient for sustaining the life I wish to live.

  3. Over time, all apertures expand. Two years ago, I had a lot of justifiable anxiety around disconnecting. Every missed email or push notification could be a meaningful dent in company health. Today, that is not true: other people read the emails, other people get paged. But the ambient demands of a growing company mean that I still spend a lot of time reacting to inputs and engaging in rituals, and instead of struggling with turning off my phone I struggle with turning off my brain. Buttondown's default velocity is upward; when I return from a weeklong trip to France, I delight in seeing that revenue has grown and dismay in seeing that a hundred little things are blocked on my input.

  4. I underestimated how gratifying impact could be, in both positive and negative ways. Twelve months ago, I had to consciously train myself to unlearn some of my bad habits from when Buttondown was smaller and single-digit minutes of downtime were a non-issue. Now, if we're down for ten minutes, we get a hundred or so emails and social mentions. The twin of this, though, is the positive: we push changes and see thousands of usages in the first week. Strangers email me thanking me for features that someone else on the team wrote, because it saves them hours a month. People have left their jobs and changed careers due to Buttondown. People cite Buttondown in essays and think pieces about software design and bootstrapped businesses — even ones published on our competitors.


All of the above is true, but it does not really communicate what my days feel like; the above is retrospective, not sensate. The reality is a maelstrom.

I wake up at 6 a.m., prep breakfast for the family, wake up Lucy and get her dressed, take Telly for a walk, bring Haley her coffee, and bike to the office around 9. Then, suddenly, regardless of my plans for the day, I am awash. KYC. A new acquisition channel has emerged that increases our net user growth by 30% but doubles our KYC burden. Someone has tried to send an email with a payload of 21MB. A firm is interested in using us, but only if we become SOC 2 compliant. A longtime user wants to hop on a call to catch up. The support team has seven escalations, three of which appear urgent. There are five new pull requests to review, two of which feel scary to merge. There are seventy new emails through which I should probably sift. There is a single bug I can knock out during lunch while eating a peanut butter and jelly sandwich. And then there are some one-on-ones and demos. Then I am biking back home and having dinner with Haley and Lucy and taking Telly for a walk, but letting Lucy hold the leash this time. And now Haley is putting Lucy to sleep and I am cleaning up the house a bit — a welcome task, because it is mechanical and physical and does not require anything beyond two legs and a smooth brain. And now it is 8 p.m. and I am very tired, but I know my body and soul are happiest when I am working out: so I chug a Celsius and bravely wade into Linear and Plain for thirty minutes while the caffeine begins to course through my veins. And then, after the workout and the protein shake, my head hits the pillow and I think to myself that I honestly could not tell you what I did today.

You repeat that around three hundred or so times, and then suddenly the business has doubled in size.

This is what life is like these days: exhausting and gratifying, both on scales that have retrospectively trivialized any prior sense of exhaustion or gratitude. I cannot physically conceive of spending my time any other way, having lucked my way into this infinite maze; and when I end my day, head on the pillow, with some level of dissatisfaction regarding the ten things I wish to have done but failed to do, I am buoyed by two things:

  1. Almost everything I did was the right call, or at least made with the right intentions;
  2. The first three faces I will see tomorrow are Telly's, and then Lucy's, and then Haley's, and I am doing right by them.
]]>
Masters of Doom https://jmduke.com/posts/masters-of-doom.html https://jmduke.com/posts/masters-of-doom.html Sun, 19 Apr 2026 00:00:00 GMT One way to approach writing about Masters of Doom is to talk about its outsized influence. Just off the top of my head: two pretty meaningful pieces of art about technology — blackberry and Halt and Catch Fire — both crib heavily from its narrative and its depictions of the early-90s technology zeitgeist. On the private-sector side, the founders of Reddit and Oculus both cite it as a core text that inspired them to start their companies. One way to approach writing about Masters of Doom is to talk about its outsized influence. Just off the top of my head: two pretty meaningful pieces of art about technology — blackberry and Halt and Catch Fire — both crib heavily from its narrative and its depictions of the early-90s technology zeitgeist. On the private-sector side, the founders of Reddit and Oculus both cite it as a core text that inspired them to start their companies.

While in 2026 some of its narratives and ideas sound a little dated or pat, it manages to be both hagiographic and educational. Kushner does a good job balancing the personality cult (though I found the cloying early chapters about the various protagonists' childhoods to be unrewarding) and the legitimate technology breakthroughs that brought id its success and fame.

This is perhaps the strongest thesis espoused by the book, which goes something like as follows: id Software was successful because it had a maniacal engineer single-mindedly focused on technological breakthroughs, and creative designers in his orbit who could leverage those breakthroughs into games beloved by millions. Everything else is incidental and auxiliary, and the alchemy of Doom and Quake's success hinged on the chimeric bond between the two Johns, neither of whom were able to replicate it independently.

In the twenty years that followed, of course, the narrative becomes a bit messier. We leave the book before Doom 3 was released, and while Kushner suggests that Doom 3 may be a middling title and that Carmack is no longer interested in engineering, he manages to both hit and miss the mark. Doom 3 was another smashing success, but id Software faded into irrelevance shortly thereafter, and the realm of first-person shooters became dominated by the antithesis of id Software: very large tech companies with embedded game studios, treating the production line like a factory floor rather than a monastery.


Romero's career after Ion Storm is hallmarked by a series of downwardly mobile steps — a fate that, if I may borrow some of Kushner's psychoanalytic inquiry, must seem a little worse than death. Having achieved fame and fortune, but not peace, and having burned through two more wives and four more studios since the book's publication.

For all the duality that Kushner tries to imbue into the narrative, this is really Carmack's story, and Carmack's arc after the book is less depressing, but more surprising. Despite vowing to never sell, id Software sold to ZeniMax in 2009, having achieved nothing notable since Doom 3's launch six years prior. Four years after that sale — and with nothing more to show for it besides perhaps a larger checking account — Carmack left to go work on Oculus as CTO, which is both a confirmation of the book's espousal of Carmack's love of VR and yet objectively a bit of a failure. Oculus never achieved anything close to mainstream success, and ten years after he joined as CTO, Carmack left Meta to work in his own personal AGI lab.

Carmack is an interesting character, and I think some of the stickiness that Kushner deploys when describing him — the autistic mannerisms, the obsession with pizza and Diet Coke — belies what is truly great. Carmack is relentlessly charitable with intellectual property. He is also, as the book describes him, a sociopath who is willing to give away his cat if it starts bothering him, and cut his friends out of a company in order to meet his ends. We know through many media of technical sociopaths, and generally associate them with greed and vanity. Carmack is not one of those people. He seems earnest and driven, and also, during the book's events, a 20-year-old who is in way over his head.


I started off this book really not liking it, and then by the end — the power of the narrative, the slow progression into the world I remembered of my youth, having never played Quake but knowing most of the personalities and zeitgeists depicted, including a US populace that was obsessed with the concept of video game violence (a concept which now seems alien) — my esteem of it kept ticking up and up, until it became a book I would generally recommend, and have done so already.

Kushner's reportage is impressive. He moved to Texas for five years to embed himself in the history and the scene, and this is not the airport book it feels like at first glance. It is not barbarians-at-the-gate, but it is something quite close.

]]>
Write as you want to be heard https://jmduke.com/posts/how-i-write.html https://jmduke.com/posts/how-i-write.html Fri, 10 Apr 2026 00:00:00 GMT If you have ever chatted with me in real life, you know that my writing style here closely reflects my speaking manner. My writing, just like my speaking, is labyrinthine and circuitous. Like a bad DFW knockoff, I manage to digress from my digressions. Sometimes it takes me minutes at a time to find my way back to what I was trying to say. If you have ever chatted with me in real life, you know that my writing style here closely reflects my speaking manner. My writing, just like my speaking, is labyrinthine and circuitous. Like a bad DFW knockoff, I manage to digress from my digressions. Sometimes it takes me minutes at a time to find my way back to what I was trying to say.

While you can debate the novelty of my writing, you cannot, in good faith, decry its earnestness. Even this post, like so many others before it, is not a laboriously manicured artifact but a rough copy edit of a transcription of a voice memo I recorded while walking my dog — the outline of which I formed in my head fifteen minutes prior as I put Lucy down to sleep.

This has good parts and bad. One good part is that perhaps more than anything else, it communicates not just the topic at hand but my overall shtick: sometimes generally, sometimes fractally. And in doing so, I sacrifice — both deliberately and out of laziness — the things that make for great writing in other disciplines: brevity, concision, citation. My essays are neither terse nor focused; if the goal of a piece of writing is to communicate its ideas as succinctly and powerfully as it can, then I very rarely meet that bar.

But while that is the goal of some writing, it is not the role of all. And certainly not the vast majority of mine. I write because it is fun to use language; I write because it is a form of thinking; I write in order to communicate with others — and that is ordered in descending priority, at least as it pertains to jmduke.com.

I do not purport to ascribe my idiosyncrasies onto you; your goals and voice are different than mine; my point is just that my style fits the goals of my writing, which is to have some fun. What if you have different (i.e. normal) goals? I recommend two tactical things:

  1. I'm stealing this from my friend Harrison, who is smarter than I am: ask yourself when writing something, is the point of this piece of writing to communicate the journey or the destination?
  2. No matter what, the advice I got from a fourth grade teacher is still the best advice I've ever received — read aloud what you've written and see how it sounds. Not just to write as you speak, but to speak as you've written.
]]>
Eyes Wide Shut https://jmduke.com/posts/eyes-wide-shut.html https://jmduke.com/posts/eyes-wide-shut.html Fri, 10 Apr 2026 00:00:00 GMT If you men only knew.

If you men only knew.

At this point, Eyes Wide Shut has so thoroughly permeated the zeitgeist that it's hard to disentangle my reaction to the film from my reaction to the world the film has spawned. It's rife with metatext — and that's not even getting into the Rothschild mansion stuff, as interesting as all of that is. It perhaps speaks poorly of me, but I had to be reminded that during filming, Kidman and Cruise were married, and that a year after its release they were divorced; I had to be reminded that this is technically an adaptation, too, and as is always the case with Kubrick what seems like masterful creation is more like masterful selection.

My vague perception of the film going in was that it was viewed as somewhat Lynchian — or at least operating in the same realm of symbology as 2001: A Space Odyssey: deep interpretations, emphasis on vibes. Some of tghat is true, but I actually feel as though very little of the film is genuinely ambiguous, and the things that are ambiguous are unimportant. Here would be my layman's read of the plot:

  • Cruise and Kidman are conventionally successful, happy people, albeit somewhat repressed in the same way many 1999 film protagonists are.
  • Cruise's sense of identity is inadvertently shattered when he grapples with the fact that Kidman is not a wife whom he has objectified but a living, breathing human with impulses and dreams — even if she'd never want those impulses and dreams to be tangible.
  • What follows is a dark night of the soul, and he discovers that the world he thought mapped and well-known contains to it secrets and evil which he had conveniently His treatment of Mandy is a bit of a lodestar. overlooked.
  • The morning comes, and after failed attempts to re-enter the night he returns to his wife, and is for the first time earnest and honest with her. This does not solve the problems of the world, but it leaves them changed — and better for it.

The film opens with Kidman nude; it ends with her clothed but telling Cruise, quite literally, that they should fuck. I think the discrepancy between these two images is more than anything else what Kubrick is trying to say, about the relationship between intimacy and nudity (or, if you'd like, between transparency and understanding.)


Beyond that, the secret society sequences felt kind of pat in a way I can't really hold against the film. The imagery was gorgeous — almost all of Kubrick's successors have failed to match it — but I knew what they were going to be, and besides on a visual level (the confrontation scene!) they no longer hold a shock or awe that I'm sure they did in the 1999 dreamscape.

More than anything else, I'm happy to have watched a film I found strange and flawed, a little overlong, but worth seeing on its own merits — and not just as a reference text.

]]>
Columbus https://jmduke.com/posts/columbus.html https://jmduke.com/posts/columbus.html Sun, 05 Apr 2026 00:00:00 GMT What he's offering is a critique of a critique. But in its place, he identifies a different kind of crisis. Not the crisis of attention, but the crisis of interest. See, to talk about attention is its own kind of distraction. Kids pay attention to things that interest them. The real question is what interests them? Or us? Are we losing interest in things that matter? Words on a page, for instance. Yeah, see, maybe that's not so important. What about everyday life? Are we losing interest...

What he's offering is a critique of a critique. But in its place, he identifies a different kind of crisis. Not the crisis of attention, but the crisis of interest. See, to talk about attention is its own kind of distraction. Kids pay attention to things that interest them. The real question is what interests them? Or us? Are we losing interest in things that matter? Words on a page, for instance. Yeah, see, maybe that's not so important. What about everyday life? Are we losing interest in everyday life?

Columbus reminded me more than anything of The Garden of Words Highly recommended, btw. and Shinkai's other short films before he transitioned to massive and somewhat loud mainstream fare like your-name. This is a painterly, deliberate film — and at times it feels as if it's tipping its hand too much, showing you just how well-considered every single frame and shot is. I didn't really mind that, though, because it is well-considered, and pleasantly so.

Kogonada's strength here is more in aesthetics and visuals than script, sometimes to poor effect. The chemistry between John Cho and Haley Lu Richardson — the latter of whom I'd probably only ever seen in The White Lotus, and both of whom I found terrific in their roles overall — is easy and pleasant, albeit mannered, not unlike a Sofia Coppola production. But the inciting meet-cute feels just a touch too incredulous; I don't really sense that either of these people, so insular in their own ways, would readily strike up this kind of conversation, nor that their relationship would progress so quickly. The contrivance of the core plot feels at odds with everything else in the film.

Compare this with Paterson, where the time we spend in Paterson's world finds him already completely established. Casey, by contrast, is a creature of transition — very sweet and smart but unconvincingly so, since we spend a lot of time hearing it from other characters rather than witnessing it ourselves. Her character is that of a dream-like abstraction, idealized and faintly abstract and slightly transparent.


Still, these are reasons I didn't love this film, not reasons I didn't think it was very good. I'd recommend it to anyone, even if the central melodrama feels faintly contrived.

]]>
L.A. Confidential https://jmduke.com/posts/l-a-confidential.html https://jmduke.com/posts/l-a-confidential.html Sat, 04 Apr 2026 00:00:00 GMT I'm not sure why, but this movie was not at all what I thought it was going to be. I don't just mean in terms of quality — though it certainly was better than I was assuming — but in terms of kind. In my head, this film was fairly generic crime fodder of the sort typical in the late nineties (perhaps it's the generic name that threw me off base). Instead, it was — and I truly don't mean this as damning with faint praise — an incredibly competent, efficient, and propulsive movie t... I'm not sure why, but this movie was not at all what I thought it was going to be. I don't just mean in terms of quality — though it certainly was better than I was assuming — but in terms of kind. In my head, this film was fairly generic crime fodder of the sort typical in the late nineties (perhaps it's the generic name that threw me off base). Instead, it was — and I truly don't mean this as damning with faint praise — an incredibly competent, efficient, and propulsive movie that does not reinvent or transcend its genre but at its best is an exemplar of it.

The three leads are all very good. I learned that this was Guy Pearce and Russell Crowe's sort of coming-out performance stateside, the one that launched their respective careers. And they both felt, in my viewing, a little too well-calibrated to the tastes of nineties cinema: Crowe a little too ridiculous as the hothead, Pearce's character a little too pat and stone-faced. But both were really fun. And it made me appreciate Crowe's performance in the-nice-guys more retrospectively, as it's very much in conversation with his earlier work here.

That being said! Spacey absolutely dog-walks the two of them. Everything that Spacey is well known for as an actor is on full display — his entire performance is a mask. And his send-off is just as compelling as what brought him there. If there's one complaint I'd level purely from a plotting-and-structure perspective, it's that the film undoubtedly loses something with his departure.


This is an adaptation of James Ellroy's once-famous novel, and it's interesting to think about what gets lost in the adaptation. The book is an epic less concerned with gangsterdom and corruption per se than with power — the structures that build it, the people who wield it, the compromises it demands. The film necessarily compresses all of that into something tighter and more conventional, and what you gain in propulsion you lose in scope. But what a propulsive thing it is.

]]>
Vanya on 42nd Street https://jmduke.com/posts/vanya-on-42nd-street.html https://jmduke.com/posts/vanya-on-42nd-street.html Sat, 04 Apr 2026 00:00:00 GMT This is, as far as I can tell, the first movie I've ever watched on the strength of an algorithmic recommendation — and the fact that it is now one of my favorite films speaks to something about how good these tools are getting. 1Certainly not taste, but correlation. This is, as far as I can tell, the first movie I've ever watched on the strength of an algorithmic recommendation — and the fact that it is now one of my favorite films speaks to something about how good these tools are getting. Certainly not taste, but correlation.

It is both correct and insufficient to describe this film as "a taping of André Gregory's adaptation of Mamet's translation of Uncle Vanya." Malle is diegetically mischievous: quick asides from the director, establishing shots of the actors as they file into a dilapidated downtown playhouse, the mundane rituals of preparation. And then the setup becomes the rehearsal (like a very gentle jump scare), and then the rehearsal becomes the play itself.

I wrote last week about stop-making-sense:

What Demme captures here is that same indelible feel of the best live music, where you feel in the same breath and beat both completely alone and completely surrounded by the only people who matter: building, building, higher, higher.

Something similar could be said here. Both films understand that the most powerful thing you can do with a camera pointed at a performance is to make the audience forget the camera exists. And, indeed, much of this enchanting two hours is in forgetting — the lack of set, the lack of wardrobe, the lack of framing device — until, at last, Brooke Smith delivers the final monologue and Gregory emerges from the shadows, and the beautiful spell is over.

(As good as that monologue was, and as great as Smith's performance is, it somehow ranks second to Drive My Car in terms of best I've seen.)

I'm sure Malle is using the staging — the crumbling New Amsterdam Theatre, street clothes instead of costumes, coffee cups on card tables — to say interesting things about the New York art scene, about the precarity and stubborn vitality of downtown theatre. Perhaps I will try hard to pay attention to that on a second viewing, in much the same way I always try hard to, upon waking from a dream, remember the next one.

]]>
Software never had a soul https://jmduke.com/posts/software-never-had-a-soul.html https://jmduke.com/posts/software-never-had-a-soul.html Thu, 02 Apr 2026 00:00:00 GMT Ryo Lu recently wrote: Ryo Lu recently wrote:

The web was the same. Personal sites were genuinely personal. Blogs felt like letters. Forums had regulars. You knew who made what. The internet had neighborhoods, and each one felt different.

Nothing was optimized for scale. Things were made by people who loved what they were making.

Somewhere along the way, we traded all of that for growth. A/B tests flattened the edges. Design systems standardized the personality out. Everything got faster, smoother, more consistent — and somehow less interesting. The quirks were removed because they didn't test well. The warmth got cut because it wasn't measurable. We optimized our way into a world of things that work perfectly and feel like nothing.

I've been turning this over in my head for a day or so, trying to pinpoint why it didn't sit well with me.

I think it's this: the narrative would have you believe that the personal web — replete with the kind of rococo and flourish that "doesn't scale" — is gone, and the mission falls on Us to bring it back. To me, this is the same kind of thinking that complains about how all the music on the radio today is overproduced poppy garbage, or that the only films coming out are high-budget, low-value, extended universe IP flicks. It is simply untrue, but the ease with which Ryo goes back and forth from talking about "software" to talking about "products" gives away the game.

I do not want my IDE to "have a soul". It is an IDE! I want it to be extremely efficient and ergonomic, and if that's at the expense of whimsy then good. I get whimsy from many other things in my life: I do not expect my OXO citrus press to contain delightful microinteractions, and Cursor (for which Ryo works) is closer to the business of making citrus presses than it is to the business of making delicious home-cooked meals.

Technology progresses at an exhilarating pace of monotonic improvement. It has never been faster, easier, or cheaper to build something unique and have it available for the entire world to see. Here are some examples I came up with in thirty seconds:

(blogroll.org has a great list of these, too.)

None of these are for companies. They are all personal websites, because the goal of a personal website is distinct from that of a corporate website — and technology has advanced such that the difference between the two is both meaningful and palpable. The personal web is not dead; it is thriving, and it is thriving precisely because the tools have gotten better, not in spite of it.


If you find yourself pining for yesteryear, remember that you do not need a time machine. You do not even need better or faster tools. You just need to really mean it.

]]>
March, 2026 https://jmduke.com/posts/march-2026-roundup.html https://jmduke.com/posts/march-2026-roundup.html Wed, 01 Apr 2026 00:00:00 GMT The chaos is finally — but not totally — starting to fade. In early March we moved back into our house, replete with new floors and a sense of something once-deferred starting to materialize. And then, as things do, the process of becoming tangible contains its own bumps. The kid gets sick. The server catches fire. One reality has been true my entire life, and yet surprises me anew each time: these kinds of things do not happen in isolation. You sit down for dinner on a Sunday thinking about how... The chaos is finally — but not totally — starting to fade. In early March we moved back into our house, replete with new floors and a sense of something once-deferred starting to materialize. And then, as things do, the process of becoming tangible contains its own bumps. The kid gets sick. The server catches fire. One reality has been true my entire life, and yet surprises me anew each time: these kinds of things do not happen in isolation. You sit down for dinner on a Sunday thinking about how little time and space has passed since the start of the week, and the answer is both "not much" and "everything."

For this and many reasons, I'm grateful for mostly existing on a monthly aperture. It's when I get the chance to zoom out a little and examine the things that have totally escaped my memory that I'm most grateful and proud. At the start of this month, Lucy was a (happy, thrilled, exuberant) shadow of who she is now; Telly had just undergone (successful!) ACL surgery and was sporting a turkey leg with a scar and now he is back at (almost) full strength and speed; our house had no floors and no furniture and now it has them.

On the Buttondown side of things, this has been the strongest month in quite some time, for reasons that I haven't had time nor reason to chase down. The things dreams are made of:

  1. Registrations went up 30% month over month (and all of the downstream funnel stuff as well);
  2. Support volume went down 5% month over month;
  3. Expenses went fairly significantly down;
  4. Our Linear backlog got decimated twice over.

And, with all that, I even had time to write (as you'll see below.)

A month is more than a daily maelstrom times thirty, even if it doesn't feel that way sometimes.

]]>
Mindwalk https://jmduke.com/posts/mindwalk.html https://jmduke.com/posts/mindwalk.html Wed, 01 Apr 2026 00:00:00 GMT Let us know how the water rises.

Let us know how the water rises.

The right way to understand Mindwalk is that it's one of the greatest education films ever made: meant not to be consumed in a theatre, but instead via VHS, inserted clumsily by a substitute teacher. At the conclusion of it, we should imagine said substitute teacher turning off the tape, blipping off the TV, and saying — pensively and tentatively, to the small coterie assembled — "so, what did you guys think?"

I'm not exaggerating when I say this film is almost exactly what would happen if you asked an LLM to write a chapter-by-chapter introduction to systems theory but in the style of the Before trilogy, and the fact that it works at all is a testament to three performances and their ability to imbue a didactic and bland script of all time with brief and lovely glimpses of flair. Some credit to be given to Mont Saint-Michel for being so breathtakingly beautiful that you are not just willing but happy to listen to Sam Waterston play faux-Socratic for 120 minutes in the foreground.

My Dinner With Andre works so well because it feels like a great conversation, with the halts and recurses and progressions. This film is The Goal — worse (for me), it's The Goal on a subject that I already know well.

]]>
A month of OpEx quick wins https://jmduke.com/posts/opex-quick-wins.html https://jmduke.com/posts/opex-quick-wins.html Sun, 29 Mar 2026 00:00:00 GMT I spent the past few weeks chasing quick OpEx wins for sport, having felt a nagging sensation that the orchards of our org were a little too-laden with low-hanging fruit. I've already written about one of them — self-hosting GitHub Actions — but here are a few more that I wanted to write down before I forget. I spent the past few weeks chasing quick OpEx wins for sport, having felt a nagging sensation that the orchards of our org were a little too-laden with low-hanging fruit. I've already written about one of them — self-hosting GitHub Actions — but here are a few more that I wanted to write down before I forget.


1. Per-seat pricing in Vercel

Buttondown uses Vercel for the docs and marketing site as well as for our Storybooks. Vercel's Pro plan requires you to pay $20/month per seat. There's a "viewer" role that you can assign to users which gives them access to view stuff, but they can't trigger deployments.

To get around this, I pushed a change to programmatically trigger deploys via GitHub Action rather than using their standard Git integration. What's more, this means we get to consolidate our CI logic a bit — which solves a long-standing annoyance of mine, having queued builds sit and wait for ten minutes only to get ignored because they're irrelevant to the given PR.

2. Sentry migration + logging

We were on an old plan that charged us way more, so I migrated us to the most recent pricing grid. (Not downgraded, mind you — just moved to the current pricing, which happened to be cheaper. The joys of being a long-tenured customer.)

After that, I took a look at the long pole in our usage-based billing, which was logs. Turns out 30% of our log volume was a duplicate wide event — one request.started for every request.finished. We never actually need those or look at them, so I removed that.

3. S3 to R2

We started using R2 for managing our frontend build tarballs that get deployed to Heroku. But really, that was a Trojan horse for a larger migration: getting off of S3 entirely in favor of R2.

R2 has a reverse backfill thing and is also compatible with S3's API, so I didn't even have to change any code — just a bunch of keys. The net result was a ~$300/month S3 bill getting chopped down to $20.

4. Obsidian Publish to Bun

While I still like Obsidian for writing internal docs, my love of their Publish plugin has come to an end. I replaced it with a hundred-line Bun build script — not unlike the one powering this very blog — and saved $10/month.

5. RQ to Postgres

Unshipped RQ entirely as a task runner in favor of Postgres. (I need to write more about this in general.) The noteworthy bit is that we went from paying a comical amount for a tiny bit of Redis hardware — roughly $250/month for two gigs of RAM — to $25/month.


All in all:

Change Before After Savings
Self-hosted CI runners $300/mo $100/mo $200/mo
Vercel seats $200/mo $40/mo $160/mo
Sentry plan + logs $200/mo $80/mo $120/mo
S3 → R2 $300/mo $20/mo $280/mo
Obsidian Publish → Bun $10/mo $0/mo $10/mo
Redis → Postgres (RQ) $250/mo $25/mo $225/mo
Total $1,260/mo $265/mo $995/mo

All in all, a latte shy of one thousand bucks a month for what was probably, in aggregate, a single day of work.

]]>
Why I'm a film blogger now https://jmduke.com/posts/film-blogger.html https://jmduke.com/posts/film-blogger.html Fri, 27 Mar 2026 00:00:00 GMT I have written more words about film in 2026 than I have about technology and business. This has not escaped your attention — so much so that real-life and internet friends alike have requested that I create an RSS feed excluding my film reviews. (By the way, I did that.) I don't think I've actually written anywhere about why I've gotten so into movies, or why I write about them, and this seems as good a time as any. I have written more words about film in 2026 than I have about technology and business. This has not escaped your attention — so much so that real-life and internet friends alike have requested that I create an RSS feed excluding my film reviews. (By the way, I did that.) I don't think I've actually written anywhere about why I've gotten so into movies, or why I write about them, and this seems as good a time as any.


First: a rule I set for myself a few years ago was that if I was going to consume anything — any media at all — I would have to write myself a little book report at the end of it, to avoid consumption for consumption's sake. This has been a great habit, and I highly encourage it. I write these for no one but myself; my readership is still overwhelmingly dominated by entrepreneurs and engineers, and I harbor no fantasy nor illusion of that ever changing.

But it feels both important and virtuous — and frankly, fun — to spend so much of my time critically writing about something that isn't what I do for work. Which is to say: the act of thinking carefully about art, of forcing yourself to articulate why something moved you or didn't, is a different muscle entirely from the one I exercise when writing about Django or pricing strategy or whatever else. It's good for you. It's good for me.


But that applies to all art. Why movies in particular? Two reasons.

One is that the form factor of a movie is very appealing to me right now. I am not a huge fan of the current state of prestige television — series are long and poorly produced, and the best ones require a commitment that frankly demands too much of me. Movies, in comparison, have a very simple value proposition: give us two hours of your time. No more, no less. There's something deeply respectful about that contract. You sit down, you surrender your attention, and in return you get a gestalt, and do with that what you will.

The second reason is the act of direction, and how interested I am lately with direction as a form of authorship. Unlike an author, a director oversees a work without being the sole individual responsible for every last piece of it: a director is a curator, an orchestrator, someone who synthesizes the work of cinematographers and actors and editors and composers into something coherent and singular. It rhymes, more than a little, with the work I do every day at Buttondown.


I'm going to keep inflicting my opinions about mid-period Mamet and Noah Baumbach and whatever else on you. If that's not your thing, the essays-only feed is right there. But I suspect some of you might find, as I have, that the best way to get better at thinking is to think carefully about something you like, and there is no better way to think carefully than to write.

]]>
Mistress America https://jmduke.com/posts/mistress-america.html https://jmduke.com/posts/mistress-america.html Mon, 23 Mar 2026 00:00:00 GMT I'm sorry, I know you liked Brooke. He told me that she worships you, she kept talking about how smart you are, how interesting...

I'm sorry, I know you liked Brooke. He told me that she worships you, she kept talking about how smart you are, how interesting...


Last year I watched Liberal Arts, which may have been the single worst quote-unquote college movie that I've seen. Lazy, boring, and incoherent. In contrast, Mistress America nails not only being a college movie, but being a New York movie and a farce with specificity, flair, and warmth, and manages to do all of these things within the confines of a 97-minute runtime. No mean feat.

I do feel like, for better and for worse, my analysis of the veracity of any of these films boils down to me coming out of the metaphorical theater thinking and then nodding my head and being like, "Yep, that's what it was like." And in Mistress America, that's what it was like. I did not have the same experience that Lola Kirke's character did. But the details were so hyper-specific and accurate, I could see so many people I knew like her from my time at William & Mary. What's more, the Greta Gerwig character serves as an equally hyper and honest depiction of that kind of late-twenties driftless coquette without ever being cruel or mean unnecessarily.

Much of this is, I think, delivered on the hands of Gerwig's performance and screenwriting. Baumbach, I think, is a director who needs Gerwig. Baumbach, I think, is a director who needs Gerwig more than the other way around.

The surrounding cast is all pitch-perfect, too — including the second-act Connecticut set, who once again are drawn with broad comedic brushes without feeling particularly flat or cardboard (another problem with most films in this genre.)

]]>
Stop Making Sense https://jmduke.com/posts/stop-making-sense.html https://jmduke.com/posts/stop-making-sense.html Tue, 17 Mar 2026 00:00:00 GMT A polite man is driven to murder. He becomes a prophet and screams manifestos on love, war, and the increasingly alarming impact of technology and progress. Driven to insanity by his own insights into the human condition, he travels to a river in an attempt to drown himself but instead is baptized and absolved of sin. He dies, crosseyed yet painless.

A polite man is driven to murder. He becomes a prophet and screams manifestos on love, war, and the increasingly alarming impact of technology and progress. Driven to insanity by his own insights into the human condition, he travels to a river in an attempt to drown himself but instead is baptized and absolved of sin. He dies, crosseyed yet painless.

This is the definitive fairytale of my generation, and the moral is "watch out, you might get what you're after". Jesus lives, and he's wearing a giant suit.

A film that is so flatly and universally beloved by all who watch it, regardless of affiliation with the band itself. And truth be told, I don't really care much for the Talking Heads — not that I dislike them or their music, but to me they are one of many bands that I can recognize the artistic and aesthetic value in at an intellectual level more than a Dionysian level. (And I don't really prefer my listening to be pleasurable on the intellectual level.)

What did I think about while watching this excellent film, a master of its genre? I thought about the greatest concerts in my life: Lost in the Trees playing in the tea house in Charlottesville, an equal number of band members and audience members; Blind Pilot playing in the Crystal Ballroom, an entirely acoustic set and an audience willing enough to go along with it; CHVRCHES at the Paramount in Seattle, sweaty and glowlit. What Demme captures here is that same indelible feel of the best live music, where you feel in the same breath and beat both completely alone and completely surrounded by the only people who matter: building, building, higher, higher.

I have half-joked with friends over the past couple years that I'm done with concerts as a medium. The event no longer holds any sort of allure outside of special occasions (once-in-a-lifetimes, family). The highest praise I can give this film is that it made me reconsider that stance.

]]>
What I would be doing https://jmduke.com/posts/what-i-would-be-doing.html https://jmduke.com/posts/what-i-would-be-doing.html Mon, 16 Mar 2026 00:00:00 GMT Tanvir asked me a few days ago what I would build today if Buttondown didn't exist and I was still keen, fully employed elsewhere. This is a fertile question, because now — more than ever — feels like the age of dwarves, both in terms of forging and mining. There are so many good things to be built, and so little time to do it, even though by some axes productivity is higher than ever. Tanvir asked me a few days ago what I would build today if Buttondown didn't exist and I was still keen, fully employed elsewhere. This is a fertile question, because now — more than ever — feels like the age of dwarves, both in terms of forging and mining. There are so many good things to be built, and so little time to do it, even though by some axes productivity is higher than ever.

There are a few different ways to answer this question, and I don't think any of them in and of themselves are complete. So I'm going to barrage him (and you) with a constellation of answers, in hopes that it approximates something approaching the truth.


An impossible supposition underpins this exercise. Do I pretend that I still know everything I've learned about Buttondown, or is this an Eternal Sunshine moment where my entire history of company-building has vanished? For the purposes of this: let's say that I was legally barred from ever touching Buttondown ever again, and go from there.


One. Something email-related — but probably not newsletters.

I did not appreciate how weird the email industry was until I really got myself waist-deep in it. Email is the largest and most ubiquitous technology in the world; it predates the internet itself, and despite all of those things, usage only continues to grow. It is, as SwiftOnSecurity put it a while back, the one promise of decentralization fulfilled.

A lot of people ask me, at various stages and venues, how I would hedge against email going away — whether it's the result of LLMs or some other hitherto nascent technology catching on. My answer is simply: I wouldn't. Buttondown is a core business, and my approach to building Buttondown is based on a somewhat opinionated thesis that email will be around forever. I'm not saying that it will be, but I'm saying that our business and my approach to building it relies on that being so.

There are a lot of bad companies in the email space, and a lot of room under the area curve to improve. Deliverability management is a great example; rendering management and things like Litmus are another. The general side effect of having an industry that is first and foremost dominated by marketers rather than by engineers is such that there is a lot of fertile ground that most people abstractly recognize but cannot act upon.

That being said! Assuming I know what I know, I don't think I would want to build a business like Buttondown again. Please don't interpret that as ingratitude or misgivings — it's been a very good time, and incredibly rewarding, both spiritually and financially. But there are two big problems with Buttondown and Buttondown-shaped businesses, the way I want to run them:

  1. They're dominated by long-tail prosumer dynamics, by which I mean you spend an inordinate amount of time working with and optimizing for a $9/month customer.
  2. Buttondown is in the business of doing a thing that is risky. Sending an email to 10,000 people carries risk. When you fuck up, there are no take-backsies. Even with payments, you can reverse a charge — you can't reverse an email.

Our stuff is so ironclad at this point that this hasn't been a meaningful issue at scale for a long time. But especially when Buttondown was first growing, I had a lot of sleepless nights worried because I changed something seventeen hours ago in our rendering or sending pipeline and I didn't know if it would subtly break something that we hadn't noticed yet.


Two. Public data.

I am fascinated by the amount of public data that is valuable and ripe for the picking. Shovel is about this, obviously, but there are many, many other examples. It should be much, much easier for you to get both broad and deep information than it currently is. I am baffled by the fact that I cannot, for example, get an email alert when one of my competitors raises pricing.


Three. Developer lifecycle and metrics.

It is no secret to anyone reading this blog that we, as an industry, do not know how to measure productivity. I do not mean this in a "slang for things that make it easier to justify stack-ranking someone" sense. I mean it genuinely: when CI times double, or flake rate triples, that hurts an organization. But we haven't yet developed the knowledge, wisdom, and praxis to understand and quantify how.

Every single company in this space ends up chasing the mirage of DORA, because that's what they can sell into the enterprise — because at least theoretically the enterprise is really the only cohort that cares about this stuff. I don't really think that's true. I think the fact that I have to bust out my dusty notebook filled with git one-liners and figure out what specific commit or dependency ballooned my overall slug size from 100 megs to 300 megs is insane. I think the fact that every single company has to more or less invent observability and anomaly tracking from flour, milk, and eggs is insane. We are very, very early in the arc of building up technology — in both the literal and figurative sense — around all of this stuff.


Four. Weird verticals.

Don't believe the chicanery about every vertical being tapped out and vertical SaaS being a dead play. It is simply untrue — a lie parroted by Yale MBAs who are unable to think their way out of a Shopify App Store roll-up.

If you have any friends whatsoever who aren't in tech, shadow them for a day — or maybe even an afternoon — and you will suddenly discover an entire universe of software designed and sold courtesy of the principal-agent problem. Insofar as I love technology because I think it creates value for people, there are few better examples of this than purely vertical SaaS, and the ability to save a gallerist or a clinician thirty minutes every week. That is virtuous.

It is also trendy to say that vertical SaaS is dead in the age of LLMs. I encourage you to think really hard about what that sentence means, and then see if you still agree with it once you're done.


Five. Payments.

No, no, no, no, no — not like that.

Stripe has finally crossed the chasm into the enterprise, a fact reflected both in their 409A valuation and the nesting structure — matryoshka meets Kafka — of their documentation page. ("To sufficiently model the universe, you must first create a meter event.")

One of our biggest areas of support burden at Buttondown is paid subscriptions, and frankly this is because we have to cosplay as Stripe's support. Stripe's Connect model is beautiful and transformative, and I'm grateful for it, as are many, many companies. But also: we expect authors and yoga instructors who have never heard of Klarna in their life to navigate an increasingly labyrinthine payments menu.

In much the same way, there are very large and robust companies that sit — like remora on the backs of whales — atop Salesforce. The Stripe marketplace and broader ecosystem is oddly thin; the Stripe App Marketplace only has 452 listings. And so on.

I have been on calls that have lasted less than an hour, for which I have billed over $1,000, purely because I know what the difference between an MoR and a PayFac is. I have, sadly and with melancholy, turned down clients who just wanted me to onboard them to billing. There is room to build not just technology but praxis on top of Stripe — and that's convenient for me, because I am already forever cursed with much of the knowledge required.

]]>
Archiving the roadmap https://jmduke.com/posts/archiving-the-roadmap.html https://jmduke.com/posts/archiving-the-roadmap.html Fri, 13 Mar 2026 00:00:00 GMT Pour one out for Buttondown's transparent roadmap, which I formally archived yesterday evening after a year or so of informal archival. This felt like the journey that so many other companies have had who have tried to keep public roadmaps and then for one reason or another got rid of theirs. Pour one out for Buttondown's transparent roadmap, which I formally archived yesterday evening after a year or so of informal archival. This felt like the journey that so many other companies have had who have tried to keep public roadmaps and then for one reason or another got rid of theirs.

Mine had nothing to do with transparency. It was entirely due to the fact that Linear now makes a much better product than GitHub does — at least for the kind of project management I need — and if there was a way to easily make our Linear publicly visible, I would be happy to do so. The third-party services and integrations which purport to offer such functionality (Productlane being the most notable) seem like more trouble and money than they're worth.

More than anything, the reason I dithered about this for so long was a false sense of worry that there would be a backlash. Around 100 or so folks have commented, watched, or reacted to various issues over the years, which is not a huge amount but not a small one either, and it felt faintly bad to leave them all in the cold.

But in reality, no one has minded or noticed that much. And whatever negative goodwill we generate from no longer having this public repository is offset by the negative goodwill we avoid from having that public repository look so obviously abandoned.

]]>
21 Bridges https://jmduke.com/posts/21-bridges.html https://jmduke.com/posts/21-bridges.html Fri, 13 Mar 2026 00:00:00 GMT A derivative, predictable, competent crime thriller. If you read that sentence and think "good," then you will like this film, and the opposite is true as well. A derivative, predictable, competent crime thriller. If you read that sentence and think "good," then you will like this film, and the opposite is true as well.

The banality points to the banality of everything about this film — it seems to avoid contrivance and missteps and misfires more than it goes out of its way to court success.

Boseman is wonderful, but his character is given absolutely nothing to do besides act with competence and rationality. The standout — the one character both written and portrayed with any sense of moral valence — is Taylor Kitsch as a trigger-happy dude who is both clearly insane but also cares deeply about his companion. When thinking about this movie I am drawn to a comparison with the-rip, given that I watched it so recently, and I find myself at least grateful for the economy in this film's runtime and its willingness to trust that the viewer is at least spending their time watching the film and not scrolling on their phone.

]]>
Paterson https://jmduke.com/posts/paterson-2016.html https://jmduke.com/posts/paterson-2016.html Tue, 10 Mar 2026 00:00:00 GMT Paterson is a film about art being a sinew in our life. Paterson has three distinct selves that we witness in the film — husband, laborer, regular at the bar — and he is satisfied with all three of these. But they live almost entirely in isolation of one another. The only connective tissue is his art. And while we have no reason to believe Paterson is anything more than an honest person, it seems fair to say that his most honest self is the one we see in the basement, late at night, nose buried...

Paterson is a film about art being a sinew in our life. Paterson has three distinct selves that we witness in the film — husband, laborer, regular at the bar — and he is satisfied with all three of these. But they live almost entirely in isolation of one another. The only connective tissue is his art. And while we have no reason to believe Paterson is anything more than an honest person, it seems fair to say that his most honest self is the one we see in the basement, late at night, nose buried in his notebook.

We are given glimpses of Paterson in other and prior contexts. Lingering shots of him in a Navy uniform. Hypothetical mirages of him as a published and celebrated poet on the horizon. But the film, like its titular character, is focused on the present and on what propels us through days that are monotonous and noble. It's beautiful because its clarity in that intent never detracts from its depth and from its conviction that art is both a thing that you put yourself into and a place to which you go.

The ending of this film is what transforms it from a meditation on life and art in a very William Carlos Williams style into something slightly brighter. In one of the few scenes that you can describe as colorful, Paterson meets a Japanese tourist reading poetry. The entire rest of the film has a banality to it that contrasts with the magical realism here — the divine tourist arriving to talk to Paterson about his world and to give him a new notebook with which to fill.

I think you could argue that a different and not entirely worse version of this film would have not included the flight of fancy here, and ended things in much the same way they began. But I will be thinking about this scene: a tourist walking away from Paterson, and Paterson then slowly walking home, composing the first poem to put in his new book (and new books are sacred things). And I will be holding that scene as close to my heart as I do the final scene of the Uncle Vanya production in drive-my-car. Because art is what it gives us. And art is what we need.


You can tell I loved this film too much to be coherent about it, can't you? One last thing — I am not sure if there's a more beautiful depiction of true love than the scene in this film in which Laura asks Paterson if the poem about their matchbook mentions the little megaphone shape the letters make.

]]>
How Buttondown's API versioning works https://buttondown.com/blog/api-versioning https://buttondown.com/blog/api-versioning Sun, 08 Mar 2026 00:00:00 GMT How Buttondown's API versioning works. How Buttondown's API versioning works.

]]>
Self-hosting our GitHub Action runners https://jmduke.com/posts/how-i-saved-100.html https://jmduke.com/posts/how-i-saved-100.html Sat, 07 Mar 2026 00:00:00 GMT Buttondown's CI runs on Blacksmith, which is a great service that I am still happy to pay for (see also this note). The impetus

Buttondown's CI runs on Blacksmith, which is a great service that I am still happy to pay for (see also this note).

But our January bill was $300, which is a lot of money for a team of our size. We merge around twenty pull requests a day, and none of them are, like, rebuilding a monorepo from scratch — most of our CI spend was going to linting and test suites that could run on a potato.

The four biggest line items broke down like this, with a long tail of a dozen or so other jobs:

Job Monthly cost
Backend tests (branch) $100
Backend tests (main) $30
Backend lint (branch) $40
Frontend tests (branch) $30

Our backend suite is genuinely heavy and parallelizable — it needs a real Postgres database and can run very quickly if given enough cores (we use Blacksmith's 8-CPU runner for this, consciously trading time for speed.)

But beyond that, everything is in the bucket of "not the long pole" and therefore relatively flexible. My mission was therefore to cut our CI bill roughly in half by throwing everything trivial on a self-hosted runner.

The hardware

I have literally had a 96GB RAM Beelink collecting dust in my office for six months, without which the economics becomes murky. I was lucky enough to buy this before prices started spiking.

The migration

My friend Myles mentioned in passing that self-hosting a GitHub Actions runner is actually easy to do, and I was skeptical in the way you're skeptical of anyone who describes infrastructure work as "easy." But they were right. The GitHub docs walk you through the whole thing: download a tarball, run a configure script, start the service. Fifteen minutes, tops, before a runner showed up as "idle" in the Actions UI.

Moving a job over is a one-line change:

 jobs:
   backend-lint:
     name: Backend lint
-    runs-on: blacksmith-2vcpu-ubuntu-2404
+    runs-on: self-hosted

One runner was enough to prove the concept, but jobs started queueing up behind each other since a single PR triggers around 16 actions. GitHub's runner application supports multiple instances on the same machine — each with its own work directory — so I set up five: pythia, pythia-2, through pythia-5. Each one is a separate systemd service. The Beelink has enough cores to handle them all without breaking a sweat.

The pain

There were lots of annoying things around permissions. One representative known issue: Docker containers create files owned by root, and then actions/checkout can't clean the workspace on the next run because the runner user doesn't have permission to delete them. Classic.

The fix is a pre-job hook — a script that runs automatically before every job:

sudo /home/jmduke/runner-hooks/fix-permissions.sh "$GITHUB_WORKSPACE"

The wrapper script only allows chown on the runner's _work/ directories (so it's not a blanket sudo), and a sudoers entry grants passwordless access to that one script.

The observability

The thing about self-hosted runners is that you lose what little visibility you get from a managed service.

So I built a little monitoring dashboard — a Flask app running on Pythia itself. It polls the GitHub Actions API every thirty seconds, stashes run data in SQLite, and serves up a web UI showing workflow history, queue depth, and which runner is handling which job. Each runner's systemd journal is streamed live via server-sent events, so I can watch jobs execute in real time without SSH'ing in.

The most actionable piece turned out to be queue time tracking. The dashboard computes how long each run waited between being created and actually starting, broken down by job type and runner environment; I arrived at five runners in parallel through the very scientific process of "get tired of waiting and seeing too many queued jobs."

The stats view: runs, duration, and queue time per day
The stats view: runs, duration, and queue time per day
The jobs view: live runner status and systemd logs
The jobs view: live runner status and systemd logs

So, flash forward a month or so to today. The whole thing looks something like this:

flowchart LR
    PR[Pull Request] --> GHA[GitHub Actions]
    GHA -->|Backend tests, deploys| BK[Blacksmith]
    GHA -->|Lint, frontend tests| P[Pythia]
    P --> R1[pythia-1]
    P --> R2[pythia-2]
    P --> R3[pythia-3]
    P --> R4[pythia-4]
    P --> R5[pythia-5]
    P -->|polls API| D[Dashboard]
    D --> SQLite
    D -->|SSE| Web[Web UI]

I originally titled this post "How to save $100 with $1000 of hardware", but it turns out this... just works very well! My relatively meagre ambitions have now morphed into "maybe we're just going to self-host the entire CI suite." It was fixed cost labor, sure, but not nearly as much as I expected.

]]>
Past Lives https://jmduke.com/posts/past-lives.html https://jmduke.com/posts/past-lives.html Thu, 05 Mar 2026 00:00:00 GMT I am legitimately struggling to articulate why Past Lives did not quite resonate with me the way it did with so many others. I found it beautiful without being indulgent — the lighting in the third act bar, for instance, or the long, luxurious tracking shot to close out the film are both stunning. The triptych of lead performances are all extremely solid, perhaps most of all that of John Magaro playing Arthur. The films that this is in conversation with — lost-in-translation and In the Mood for...

I am legitimately struggling to articulate why Past Lives did not quite resonate with me the way it did with so many others. I found it beautiful without being indulgent — the lighting in the third act bar, for instance, or the long, luxurious tracking shot to close out the film are both stunning. The triptych of lead performances are all extremely solid, perhaps most of all that of John Magaro playing Arthur. The films that this is in conversation with — lost-in-translation and In the Mood for Love — are major touchstones for me.

And yet something just didn't quite hit, and left the film merely at the status of admiration rather than love.

Perhaps it was the neatness of the framing device and the way we don't quite spend enough time with our central character in any one era to have a really strong and intimate understanding of who they were and are. Song, playwright by training, deploys a bizarre barbell of extreme economy punctuated by monologue. Nora, her autofictional insert, is an oddly thin character — she has little chemistry with either of the male leads and little interiority beyond that of her career goals. This might be the pleasantness of the film's aesthetic working against it: in a quest to make every shot subdued and beautiful, Song smooths away the knots and edges that make things feel alive.

That's too harsh for how I feel — this was a lovely film, just left me a little colder than I was expecting.

]]>
The road to Pydantic V2 https://jmduke.com/posts/pydantic-v2.html https://jmduke.com/posts/pydantic-v2.html Wed, 04 Mar 2026 00:00:00 GMT Around 18 months ago, I wrote: Around 18 months ago, I wrote:

Cursor did both! ...and then it choked on some more complicated feature work that spanned multiple files. Which is fine: a tool does not need to be flawless to be useful, and Cursor proved itself useful.

I was sufficiently impressed with Cursor for handling an operation that only required the context of a single file.


Earlier this week, it did something substantially larger and migrated our entire codebase from Pydantic v1 to Pydantic v2 and with it from Django Ninja v0 to Django Ninja v1. The total diff of this operation was +23,885,-19,931: much of that is codemod / autogen, but you get the point. It was not a trivial thing.

My total sum prompt for Opus 4.6 in doing this:

Migrate us to Django Ninja V1

The PR was not perfect, but it choked on things that I suspect I or any other human would have equally choked on. Notably, the fact that we incorrectly assumed some keys in some dictionaries were strings, and that we serialized some schema in esoteric places that weren't being tested. None of the bugs introduced caused meaningful downtime or customer impact at all. In my head, this would have been an entire week of my time and effort to do by hand.

18 months!

]]>
Singles https://jmduke.com/posts/singles-future-islands.html https://jmduke.com/posts/singles-future-islands.html Wed, 04 Mar 2026 00:00:00 GMT I learned of this band, as many people did, through the Letterman performance. And it is as stunning as the internet commentary leads you to believe. The album itself, though, leaves a little to be desired. It is samey — not in a particularly bad way, but in the sense that it feels like variations on the same song rather than a gestalt in of itself, as if they were hoping all ten tracks would get licensed by different auto manufacturers. It was not an unpleasant listen, but I think I will stick... I learned of this band, as many people did, through the Letterman performance. And it is as stunning as the internet commentary leads you to believe. The album itself, though, leaves a little to be desired. It is samey — not in a particularly bad way, but in the sense that it feels like variations on the same song rather than a gestalt in of itself, as if they were hoping all ten tracks would get licensed by different auto manufacturers. It was not an unpleasant listen, but I think I will stick to the single, as the name suggests.

]]>
Ascensions https://jmduke.com/posts/ascensions.html https://jmduke.com/posts/ascensions.html Mon, 02 Mar 2026 00:00:00 GMT Most procedurally generated roguelikes have a concept of ascending difficulty levels designed to test the mettle of players who have wasted the most time mastering the nuances of the game. (Lest you think I speak in disparagement, I have cleared A20 for every character in Slay the Spire.) Most procedurally generated roguelikes have a concept of ascending difficulty levels designed to test the mettle of players who have wasted the most time mastering the nuances of the game. (Lest you think I speak in disparagement, I have cleared A20 for every character in Slay the Spire.)

Some of these difficulty modifiers operate by adding things you have to contend with, but the more interesting ones are subtractive. They take away tools from your tool belt, forcing you to be more resourceful with what remains.

It is in that spirit that I suggest a few exercises to get yourself out of muscle memory.

  1. Go an entire day without looking up any data in any vendor's interface.

  2. Institute a rule that all changes to the codebase must be net negative over a certain horizon. To land a PR that adds 400 lines of code, you must first separately remove 400 lines of code. I say separately because it is probably bad git hygiene to have the two commingled, unless they happen to be touching the same code path.

  3. For an entire week, you are not allowed to answer any support ticket with any information that isn't publicly accessible within the docs.

  4. Inspired by Netflix's Chaos Monkey, build and run a script that turns off one of your third-party dependencies and returns 500s.

  5. For an entire week, enable network throttling in your browser of choice.

  6. Go through your primary activation flows on your phone with adaptive text cranked to its largest setting.

  7. Grab ten users who have churned more than three months ago out of a hat and email them, all lowercase, with the subject: "howdy — anything we can do to win back your business?"

  8. Go an entire week without logging in as a given user. If you're trying to replicate some tricky bit of state, you are only allowed to pull what you think is the relevant data pertaining to them from the production database.

  9. Delete five third-party dependencies, open source or otherwise, by the end of the week.

  10. Do that one thing that you've been putting off — you know what it is — before doing anything else.

]]>
February, 2026 https://jmduke.com/posts/february-2026-roundup.html https://jmduke.com/posts/february-2026-roundup.html Sun, 01 Mar 2026 00:00:00 GMT Last month's Wednesday update, I recorded from a train headed to Middelburg. This month I write closer to home temporally and otherwise. I am en route to the office on a very early Friday morning. We are still eight days from being able to return home with a new set of floors and an absent population of termites awaiting us. Eight days is not so far away. In fact, I have to remind myself a few times every day to make sure the message sticks. Last month's Wednesday update, I recorded from a train headed to Middelburg. This month I write closer to home temporally and otherwise. I am en route to the office on a very early Friday morning. We are still eight days from being able to return home with a new set of floors and an absent population of termites awaiting us. Eight days is not so far away. In fact, I have to remind myself a few times every day to make sure the message sticks.

I mentioned that I'm going in early. It is currently six in the morning. We're staying with my parents out in the West End, and the office has a distance to it now that robs itself of much of its novelty. Half of the value I placed in it was its Goldilocks nature of being just far away from home to feel like a true second place without actually imposing any tax on the distance traveled. Obviously, it is privileged of me to say that a 30-minute commute is odorous. But the reason why I'm going early is to avoid some of the traffic. For the past few weeks, I've been moving up my schedule a couple hours to spend afternoons with Lucy. It's easy to forget too how lucky I am to be able to do this.

This serves as a good metonymy for February writ large. Reminders of luck and flexibility in having parents happy to host us for weeks on end. And in having uncles excited to spend languorous long weekends with their niece. Lucky for a child who wants for nothing and ends every day with a smile on her face. Lucky for a wife who can move mountains and carry rivers. Lucky for time at all to write, to think, and to hit send before going on with my day.

]]>
Unshipping Keystatic https://jmduke.com/posts/unshipping-keystatic.html https://jmduke.com/posts/unshipping-keystatic.html Sat, 28 Feb 2026 00:00:00 GMT Two years after initially adopting it, we've formally unshipped Keystatic. Our CMS, such as it is, is now a bunch of Markdoc files and a TypeScript schema organizing the front matter — which is to say, it's not really a CMS at all. Two years after initially adopting it, we've formally unshipped Keystatic. Our CMS, such as it is, is now a bunch of Markdoc files and a TypeScript schema organizing the front matter — which is to say, it's not really a CMS at all.

There were a handful of reasons for this move, in no specific order:

  1. Our team's use of Keystatic as an actual front-end CMS had dropped to zero. All of the non-coders have grown sufficiently adept with Markdown that the GUI was gathering dust; Keystatic had become a pure schema validation and rendering tool, and offered fairly little beyond what we were already getting from our build step.
  2. Some of the theoretically nice things — image hosting, better previewing — either didn't work as smoothly as we'd like or were supplanted entirely by Vercel's built-in features.
  3. The project appears to have atrophied a little bit, commits dwindling into the one-per-quarter frequency despite a healthy number of open issues. This is not to besmirch the lovely maintainers, who have many other things going on. But it's harder to stick around on a library you're not getting much value from when you're also worried there's not a lot of momentum down the road.

That last point is basically what I wrote about Invoke — it's a terrible heuristic, judging a project by its commit frequency, and I know that. Things can and should be finished! And yet. When you're already on the fence, a quiet GitHub graph is the thing that tips you over.

To Keystatic's credit, it was tremendously easy to extricate. The whole migration was maybe two hours of work, most of which was just deleting code. That's the sign of a well-designed library — one that doesn't metastasize into every corner of your codebase. I wish more tools were this easy to leave.

]]>
How we check every link in your email https://buttondown.com/blog/link-checking https://buttondown.com/blog/link-checking Sat, 28 Feb 2026 00:00:00 GMT How Buttondown checks every link in your email. How Buttondown checks every link in your email.

]]>
Somewhere https://jmduke.com/posts/Somewhere.html https://jmduke.com/posts/Somewhere.html Wed, 25 Feb 2026 00:00:00 GMT Somewhere is a film that on the surface level feels and sounds like a complete retread. The log line is as cliché as it gets: a famous but unhappy actor re-evaluates his life priorities after an extended period of time with his eleven-year-old daughter. Somewhere
Somewhere

Somewhere is a film that on the surface level feels and sounds like a complete retread. The log line is as cliché as it gets: a famous but unhappy actor re-evaluates his life priorities after an extended period of time with his eleven-year-old daughter.

That's all this movie is. It's very much Lost in Translation, except swap Tokyo for LA and swap an implicit parental relationship for an explicit one. Steven Dorff is a little bit earlier on the age curve than Bill Murray, but they're still in the same place — having arrived and realized they're nowhere.

I would not expect many people to love this film. It is, I think, a bit gratuitous with Coppola's clichés: a lot of intentionally overlong shots with fixed apertures, a lot of too-cute musical cues, a lot of emphasis on the lifestyles of the rich and famous. Action is not even secondary to plot; it's tertiary at best.

And yet, I really enjoyed this movie. I thought every bit of it was successful. It accomplished everything it tried to do, and the only serious criticism you could level at it is that it didn't really try to do anything new relative to her work up to this point.

The film only exists in any capacity based on the wattage and nuance of Dorff and Elle Fanning's performances — as individuals and as a unit. And I'm not sure what there is to say besides the fact that they knocked it out of the park. I think playing a character in a Coppola film is not unlike playing a character in a Wes Anderson film: you're asked to filter yourself through an affectation that might mute you entirely. But Dorff's physical presence — he absolutely nails it. I don't think I've seen him in anything before, and a quick scan of his filmography suggests that I won't see him in anything after this. But the kind of crisp crap-except-a-bit-of-a-sleaze energy, much more interested in uppers and downers than in introspection, he just nails. He's neither sympathetic nor unsympathetic. He makes sense as a person.

I'm also left with the feeling that BoJack Horseman took and cribbed a lot from this movie — which is perhaps giving this movie too much credit relative to the wide pool of art set in LA about a famous actor who's nonetheless depressed.

]]>
Scattered thoughts on LLM tools https://jmduke.com/posts/five-observations-ai-tools.html https://jmduke.com/posts/five-observations-ai-tools.html Tue, 24 Feb 2026 00:00:00 GMT ChatGPT 5 is an incrementally better, higher-quality experience than its predecessors, and it lets you use an LLM in many different ways. But as a piece of software, it's absolutely bananas how busted it is—and I think we've all gone so far down the rabbit hole that we're not seeing it.
  • Claude Desktop is a poor app in bizarre and inexplicable ways — stale table view cells, constant reauthentication requests, a markedly worse harness and response rate. I am reminded of Paul's excellent review of GPT-5, in particular this passage:
  • ChatGPT 5 is an incrementally better, higher-quality experience than its predecessors, and it lets you use an LLM in many different ways. But as a piece of software, it's absolutely bananas how busted it is—and I think we've all gone so far down the rabbit hole that we're not seeing it.

    1. Cursor's roadmap is best understood by what their most prominent ICs are posting on X the everything app, which right now is cloud agent workflows. When everything else is just so, it seems like the logical endpoint is infinite and perfectly abstracted sandboxes with previewing, isolation, and very tight feedback loops. But right now the largest gap between where we and most other organizations are and that brilliant future is not on the AI side but on all the calls from coming inside the house that make it difficult to sandbox a mature application.

    2. I am still loving Conductor; it is the interface through which I do the majority of my LLM experimentation, and yet none of my long-term fears about their business prospects as outlined in that essay have been quelled. I, right this very second, find Conductor valuable enough to pay for; I don't think I represent the majority.

    3. Internal LLM tools are having a bit of a moment in the spotlight. Ramp pushed theirs for a couple PR cycles (Why we built our background agent) and Stripe is now attempting to do the same (Minions: Stripe's one-shot end-to-end coding agents). Naturally, there are 100 or so independent projects on GitHub that are trying to recreate the behavior.

    4. Everyone has built AI code review; nobody's made it stick. Linear is actively working on code review; GitHub, the pre-eminent market leader, has somehow destroyed their own app to the point where I am prompted, on a twenty-file pull request, to instead view each of those files on its own page to improve performance.


    I am sure the landscape will look different in a few months' time, but not tremendously so. Everyone appears to be coalescing on the same handful of truisms from slightly different vantage points:

    1. LLMs that run in sandboxed cloud contexts are more horizontally scalable;
    2. LLMs work best when as much extracurricular data as possible is provided;
    3. Improving the feedback loop of an LLM is as important (if not more so) than improving the LLM itself;
    4. The chokepoints in high-level processes are not yet being addressed by LLMs in a systemic way.
    ]]>
    What Happened Was https://jmduke.com/posts/what-happened-was.html https://jmduke.com/posts/what-happened-was.html Tue, 24 Feb 2026 00:00:00 GMT Two of my absolute favorite films of all time, albeit for very different reasons, are My Dinner with Andre and Before Sunrise. Both of these films, which I highly encourage you to watch more than anything else I talk about if you haven't already done so, are about the enchantment and sucker of one single really interesting conversation. The two films diverge pretty heavily from there. My Dinner with Andre is a film about work, fulfillment, and status. And Before Sunrise is a film about youth... Two of my absolute favorite films of all time, albeit for very different reasons, are My Dinner with Andre and Before Sunrise. Both of these films, which I highly encourage you to watch more than anything else I talk about if you haven't already done so, are about the enchantment and sucker of one single really interesting conversation. The two films diverge pretty heavily from there. My Dinner with Andre is a film about work, fulfillment, and status. And Before Sunrise is a film about youth in love. But the beauty in both comes from not just their simplicity and formless structure, but in the recursive nature of the dialogue, just like in real life, where a pregnant pause or a sidelong glance suddenly carries with it enormous weight after understanding not just the comment but the 75 minutes preceding it.

    What Happened Was is interested in that last thing too. And in the unraveling of yourself that happens when you spend time being intimate in a literal sense with anyone. But is more interested in a funhouse mirror look at the human psyche. And has perhaps more cynical and caustic things to say about the way people express themselves through others. Our dual protagonists are a paralegal and an executive assistant. Both seem a little off, but not wholly so. And then, over the course of the worst first date in the world, we watch the characters reduce themselves to mania.

    This is an uncomfortable film to watch. Rather than transposing yourself into Andre and his counterpart, or Jesse and his counterparty, you find yourself just kind of internally screaming on behalf of both characters who have a Lynchian sense of bizarre behavior. In terms of inspiration, this draws more from Waiting for Godot than Who's Afraid of Virginia Woolf. The dread you feel is less from a place of sadness and understanding and more from a sense of shock and increasing bewilderment. And to that extent, it flatly did not work for me quite as much as I hoped.

    But as in all two-part plays, the film ends with two monologues, one from each character, where they lay bare the things that at that point are almost nakedly obvious to us, the viewer. And while I can't say either monologue or scene was particularly well written, I will say that both of them will stick with me for a long, long time. (I'm not sure the preceding seventy minutes earned those monologues, but that's a point beside.)

    ]]>
    Golinks https://jmduke.com/posts/golinks.html https://jmduke.com/posts/golinks.html Mon, 23 Feb 2026 00:00:00 GMT If you've never encountered golinks before: they're short, memorable URLs that redirect to longer ones. Instead of telling a coworker "the dashboard is at https://app.example.com/internal/analytics/v2/dashboard?org=us," you just say go/dashboard. Instead of bookmarking seventeen different Notion pages, you type go/onboarding or go/roadmap or go/expenses and trust that you'll end up in the right place. If you've never encountered golinks before: they're short, memorable URLs that redirect to longer ones. Instead of telling a coworker "the dashboard is at https://app.example.com/internal/analytics/v2/dashboard?org=us," you just say go/dashboard. Instead of bookmarking seventeen different Notion pages, you type go/onboarding or go/roadmap or go/expenses and trust that you'll end up in the right place.

    flowchart LR
        U["go/dashboard"] --> R["Golink resolver"]
        R --> D["https://app.example.com/internal/analytics/v2/dashboard?org=us"]
        U2["go/pr/1234"] --> R
        R --> D2["https://github.com/acme/repo/pull/1234"]
        U3["go/expenses"] --> R
        R --> D3["https://app.expensify.com/reports"]
    

    I discovered them at Stripe, though I believe they were invented at Google, and I have not stopped using them since.


    One thing leads to another. You decide that you no longer need Tailscale because the main reason you spun up Tailscale was for a project that ended up shipping — and therefore spending per-seat pricing on a service that you literally only use for golinks seems a bit silly and prohibitive. Side note: I still really love Tailscale and think it's a great product and would be shocked if we aren't using it again by the end of the year. But!

    And then you need to find a replacement for golinks, and you cannot get dragged back to golinks.io or Trotto, both of which are slow, cumbersome, and expensive besides.

    So what was I to do? First, I looked at the open source options, none of which struck me as particularly compelling. I have a set of requirements that I don't think are esoteric, but others might:

    1. A reasonable price
    2. Persistence
    3. The ability to use golinks without a Chrome extension
    4. Variables
    5. Performance

    And nothing quite fit the bill.


    I had a revelation: I discovered that you could use a default search engine as the routing proxy instead of /etc/hosts or DNS interception like Tailscale's MagicDNS. For a week or two, I had this sitting within Django in our monorepo out of ease — simply intercept any incoming search query, redirect it if something's already in the database, and then if it's not but it looks like it could be, send to the empty state prompting the user to create a golink.

    But frankly, this was just slower than I wanted. Not for any interesting reason, but just the usual Python request-response lifecycle stuff. I could, of course, invest in making it better and faster and was planning on doing so, but figured I would take one last trip around the internet to see if there was some other solution that I somehow missed.


    And that's when I discovered GoToTools.

    There is nothing really interesting to say about this product besides the fact that it is very good for what it does. Its author appears to have built it out of the same frustration that I had. And the highest compliment I can give it is that in a year where I've already cut down substantially on the number of services I pay for — in favor of those that I vend — I have absolutely no compunction about starting to use this. The pricing is extraordinary. The performance is really good. It works and is fast and lets me not spend time thinking about golinks and instead lets me spend time using them.

    ]]>
    Improper nouns https://jmduke.com/posts/improper-nouns.html https://jmduke.com/posts/improper-nouns.html Sun, 22 Feb 2026 00:00:00 GMT Myles wrote a great post about standing up and scaling our nascent recommendation engine. Buried in the middle is an aside which, as you might suspect, is near and dear to my heart: Myles wrote a great post about standing up and scaling our nascent recommendation engine. Buried in the middle is an aside which, as you might suspect, is near and dear to my heart:

    There are effectively seven separate medium vocabularies in our system... Netflix has a taxonomy team of 30 people whose entire job is maintaining genre tags.

    Words are tricky and important. Words are weightless and load-bearing.

    I wrote about the "newsletter" problem last week in how-id-grow-buttondown: if you say newsletters, it's a very specific word that works for some of your customer cohorts but turns off your business users; if you say campaigns, you alienate the prosumer cohort. Yuck!

    And this week I was dealing with another such problem, one I have been dealing with for years: Email somehow means both of our most important primitives at once: an email is a broadcast sent out, and an email is also the thing that a subscriber has. At least in this case we can save ourselves a bit of pain and refer to emails in the past tense as archives in some places. But then you run into inconsistency, and users start thinking that an "email" in our parlance is something different than an "archive." Lest you think this is a theoretical example, I can point you to dozens of actual tickets as exhibits that it is not.

    In the codebase about which Myles writes, the core user — an art gallery — is referred to as one of a platform, a widget, a gallery, a user, an admin, and probably three or four others that I'm forgetting. I have a Sapir-Whorf-esque belief that how you architect code — from big lofty ERDs down to individual variable names — influences how users experience that code. All the more reason that durable abstractions are extremely, extremely important.

    And there is no stronger abstraction than a word.

    ]]>
    Perfection https://jmduke.com/posts/Perfection.html https://jmduke.com/posts/Perfection.html Sun, 22 Feb 2026 00:00:00 GMT They did for money now what they used to do out of passion. This was a fact. From this fact they concluded that they had turned their passion into a job. This was a deduction.

    They did for money now what they used to do out of passion. This was a fact. From this fact they concluded that they had turned their passion into a job. This was a deduction.

    They would imagine how they must look to the outside world with their aching cheekbones drawn into fixed grins, their clothes smeared with cigarette ash and sweat, and still carrying the odd trace of dimly remembered adventures: a marker pen scribble on their face; a garland of fake frangipani in their pocket; a bunch of helium balloons tied to their jacket buttons and now trailing, half-deflated, like comet tails. They would feel decadent and enviable, alive.

    All the creature comforts in the world can't keep it from being depressing. You find yourself taking ketamine in your late 30s and half-heartedly trying to get a bouncer to let you into a party. Whether you find this revelation and many others like it the told to you in biting mockery or bruising self-flagellation depends on how much winking autofiction you decide to glean from Vincenzo Latronico's Perfection.

    This is a book, a couple named Tom and Anna, who by all accounts are the picture of happiness. They have an email job. They live in Berlin of their own volition. They are very aesthetic and get to lead the lives they always thought they'd want, filled with tasteful furniture and art gallery openings, hanging out with the cool kids. Tom and Anna moved to Berlin from Italy and appear to have no real discernible personality or distinction from one another, a fact that would have made this a fairly boring book save for the fact that I learned that its author had moved and lived in Berlin. We are meant to hate Tom and Anna, but also to pity them. I find it easy to do the latter and harder to do the former, mostly because, despite the author's entreaties otherwise, I cannot condemn anyone for falling prey to consumerism and to surface-level materialism that forever encroaches on society, or so it feels.

    The book's main action, such as it is, is a dawning realization for Tom and Anna that despite their titular perfect lives, they find themselves increasingly unhappy and adrift with work, with meaning, with their place in Berlin, with their place in the world. This is perhaps the mode of life that I am in, but to me, the message of the book is clear. Tom and Anna have rejected their family, both as their parents and their theoretical children, and are suffering a crisis of faith as a result. But there's a certain Wormwood's Letter didacticism to the prose and message that would be unbearable if the novel were any longer than its 137 pages.

    Vincenzo is a very talented writer and a keen observer of all the accoutrement that a well-informed and well-to-do elder millennial would consider correct. I found myself thinking of Patricia Lockhart's No One Is Talking About This, which did a similarly impressive job of writing about online without fully succumbing to it. But Lockhart's book turned when its protagonist realized in its second half what the real world had in apposition to that of the Digitus. Here we get no such antidote, and the book ends less with a note of rejection of consumerism and more on a scathe, that of Tom and Anna through luck but not will ascending the rungs from Lucky Labor to capital.

    I led this essay by saying your view of its message is defined by how much you think this is the author attacking himself versus attacking his erstwhile companions in the faux-literate Berlin scene. Maybe it's just projection and the huge Perec epitaph from Things, a book that this is apparently much modeled after. But I can't help thinking that Vincenzo is attacking the things which he's transcended more than he's commenting on his transcendence, and that forces me to view the work in a dimmer light. His reviews of Tom and Anna give off the uncharitable and mean-spirited stench of a quote tweet. They have bad sex, they bicker, they form no opinions for themselves. They can't even really help during a migrant crisis.

    All which I say many such cases I cannot bring myself to despise someone who I would probably identify with in spirit but in solidarity. I would probably be doing the digital nomad thing if I weren't lucky enough to have Hayley and then Lucy roll themselves into my life. It feels hardly virtuous to begrudge someone whose greatest sin was to not be as lucky as I was.

    ]]>
    Changelog mornings https://jmduke.com/posts/changelog-mornings.html https://jmduke.com/posts/changelog-mornings.html Sat, 21 Feb 2026 00:00:00 GMT A new addition to my routine has been to start every morning writing out our changelog from the day before. This is mildly surprising to people who assume that an LLM is responsible for the changelog's lovely and repetitive prose. I have no deep compunction about using an LLM for such purposes, but in my experience, it is actually not particularly good at separating wheat from chaff and is more than happy to understandably assume that the 1,000 lines of code for a feature that has not yet ac... A new addition to my routine has been to start every morning writing out our changelog from the day before. This is mildly surprising to people who assume that an LLM is responsible for the changelog's lovely and repetitive prose. I have no deep compunction about using an LLM for such purposes, but in my experience, it is actually not particularly good at separating wheat from chaff and is more than happy to understandably assume that the 1,000 lines of code for a feature that has not yet actually launched is more important to highlight than the 20-line subcode that fixes or improves something in the editor. More than that, it's a useful centering exercise, not just to remind myself with the distance of a good night's sleep what we actually did the day before, but also what we didn't do. There is nothing quite like going commit by commit, forcing yourself to reckon with the work that really gets prioritized.

    And for the most part, I'm happy with the work that gets prioritized. We spend a lot of time on paper cuts, some of which, yes, come from going too fast, but most of which come in the natural and obvious way that afflicts all applications. And we spend a lot of time on meta work too, but the private section of the changelog is dwarfed by the public one.

    ]]>
    More weird tests https://jmduke.com/posts/weird-tests-2.html https://jmduke.com/posts/weird-tests-2.html Sat, 21 Feb 2026 00:00:00 GMT It has been a while since I wrote about weird tests. This is not due to lack of enthusiasm — if anything, I think my passion for them has redoubled over the past eighteen months. It has been a while since I wrote about weird tests. This is not due to lack of enthusiasm — if anything, I think my passion for them has redoubled over the past eighteen months.

    The construct and appeal of programmatically declaring invariants about your codebase and gating CI on them is, for reasons that hopefully do not require elaboration, more appealing than ever. Here are a handful that I've grown fond of:

    • A test to make sure that every single partial and SVG in your codebase is imported at least once. knip is a great tool for this in the JavaScript ecosystem, but it turns out that there are other ecosystems as well.
    • A test to make sure that every new model has at most one status field.
    • A test for making sure that the current commit has at most one migration.
    • A test for ensuring that all migrations have a reverse operation associated with them.
    • A test to make sure every localization string is meaningfully different — not just different due to punctuation or pluralization.
    • A test to make sure example.com is never used as, well, an example.

    In much the same way I use regression tests as a no-nonsense way to grow a conventional test suite — if something's broken, add a test and then fix it — I've started using weird tests as regressions for things that I feel silly flagging in code review or catching in prod. It's a useful tool, and I'm happy I've developed the muscle memory to reach for it.

    ]]>
    Maybe use Plain https://jmduke.com/posts/maybe-use-plain.html https://jmduke.com/posts/maybe-use-plain.html Fri, 20 Feb 2026 00:00:00 GMT When I wrote about Help Scout, much of my praise was appositional. They were the one tool I saw that did not aggressively shoehorn you into using them as a CRM to the detriment of the core product itself. This is still true. They launched a redesign that I personally don't love, but purely on subjective grounds. And there's still a fairly reasonable option for — and I mean this in a non-derogatory way — baby's first support system. When I wrote about Help Scout, much of my praise was appositional. They were the one tool I saw that did not aggressively shoehorn you into using them as a CRM to the detriment of the core product itself. This is still true. They launched a redesign that I personally don't love, but purely on subjective grounds. And there's still a fairly reasonable option for — and I mean this in a non-derogatory way — baby's first support system.

    I will call out also: if you want something even simpler, Jelly, which is an app that leans fully into the shared inbox side of things. It is less featureful than Help Scout, but with a better design and lower price point. If I was starting a new app today, this is what I would reach for first.

    But nowadays I use Plain. Plain will not solve all of your problems overnight. It's only a marginally more expensive product — $35 per user per month compared to Help Scout's $25 per user per month. The built-in Linear integration is worth its weight in gold if you're already using Linear, and its customer cards (the equivalent of Help Scout's sidebar widgets) are marginally more ergonomic to work with. The biggest downside that we've had thus far is reliability — less in a cosmic or existential sense and more that Plain has had a disquieting number of small-potatoes incidents over the past three to six months.

    My personal flowchart for what service to use in this genre is something like:

    1. Start with Jelly.
    2. If I need something more than that, see if anyone else on the team has specific experience that they care a lot about, because half the game here is in muscle memory rather than functionality.
    3. If not, use Plain.

    But the biggest thing to do is take the tooling and gravity of support seriously as early as you can.

    ]]>
    On the Calculation of Volume (Book I) https://jmduke.com/posts/on-the-calculation-of-volume-book-i.html https://jmduke.com/posts/on-the-calculation-of-volume-book-i.html Thu, 19 Feb 2026 00:00:00 GMT I was ready to wash my hands of this book very early on. I've seen enough vaguely auteurist time loop art like Palm Springs or Russian Doll to consider myself sated with the concept, and no amount of Long Live the Post Horn-esque Scandinavian quietude I thought could rescue a conceit that I was tired of as soon as I understood the game. But that is not what this book is about. I was ready to wash my hands of this book very early on. I've seen enough vaguely auteurist time loop art like Palm Springs or Russian Doll to consider myself sated with the concept, and no amount of Long Live the Post Horn-esque Scandinavian quietude I thought could rescue a conceit that I was tired of as soon as I understood the game. But that is not what this book is about.

    Click to reveal spoilers

    From here on out, I will talk in terms of spoilers.

    The crux of the book is simple at a textual level. If you got stuck in a time loop, you would probably go insane. What I found so striking about this fairly banal observation is that the author resists easy outs. There is no third act in which the whole thing starts to get solved, nor is there a narratively convenient progression from discomfiture to hedonism to malaise to revelation. The protagonist in epistolary form shows us how she's experienced all of these things at once and that you do not simply get over any one bit of it and move on to the next. You are trapped in a new and terrifying state, not watching the world pass you by but drifting further and further away from the world. A helium balloon, which you should have clung on to tighter.

    I admire Balle going out of her way to reject any pretense of theory crafting. And that too is part of the book's charm. You spend the first half or so trying to suss out the riddle before the protagonist does, and then you, like the protagonist herself, come to the horrifying realization that there is no internal logic. No magic key to unravel the whole thing. At a subtextual level too, the book resists easy interpretation and is more of a Rorschach test. You could interpret TNT's obsession with antiquary material, for instance, as meaningful, or the fact that the anchoring interaction of the book was a lovely and platonic dinner with a married couple which seems to be in the throes of life and love. This is a book about grief and loss without having to be obvious or reductive.

    The book ended on such a strong and yet dissonant note that I was surprised to learn that the Book I in the title is not a bit of fanciful artcraft, but this is in fact the first of a seven-book series, three of which are already out. I'm not sure if that cheapens my experience of this one, which stands alone as an incredible read—a soft and melancholy lens into what really matters about the passage of time. But I know without a doubt I'll be picking up the second volume.

    ]]>
    The Family Fang https://jmduke.com/posts/the-family-fang.html https://jmduke.com/posts/the-family-fang.html Thu, 19 Feb 2026 00:00:00 GMT You might not believe it, but I studied at my university. I studied in the experimental theater wing. So your parents are just two of the most important avant-garde artists of our time.

    You might not believe it, but I studied at my university. I studied in the experimental theater wing. So your parents are just two of the most important avant-garde artists of our time.

    The Family Fang, as far as I can tell, has no cultural footprint. Despite starring Christopher Walken, Jason Bateman, and Nicole Kidman, I have never heard a single person in real life or otherwise discuss its existence. It feels at the surface level like so many other vaguely independent dramedies that serve as scheduled filler for large but not enormous actors in the 2010s before the rise of streaming supplanted such vehicles. (I say feels like and not is because Bateman directed this and Kidman produced it.)

    This is a rich text — which is an odd thing to say about a film that I did not find to be particularly good. There are a lot of moments that more than anything else feel deeply real and correct. And there's a wisdom and maturity in how it portrays the realization that your parents are people too: weird and flawed and miserable and misguided, with aspirations that they know will likely never be fulfilled, but who also exist in a big universe outside of the comforts of your home. There are so many moments of interstitial profundity in glances and asides — likely owing to the richness of its source material, a book of the same name — that you are tempted to forgive the film's sins: its atonality in jumping from mystery to comedy to sweetness.

    The final act was bizarre in a way that I will still be digesting, not because I need to decipher its meaning, but because I need to decide if I like it or not.

    The final scene is Bateman's character Baxter having finally unshackled himself from his parents — who, it must be said, should be qualified as abusive on charges of incest alone — and having finished his third book, reading it out loud. The name of the book is The Children's Pit, and the prose is truly awful. It sums up the film neatly, though I can't decide if that is meant to be noticed or not.

    ]]>
    Subpaths vs. subdomains https://jmduke.com/posts/subpaths-vs-subdomains.html https://jmduke.com/posts/subpaths-vs-subdomains.html Wed, 18 Feb 2026 00:00:00 GMT 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). 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.

    ]]>
    Outgrowing Django admin https://jmduke.com/posts/outgrowing-django-admin.html https://jmduke.com/posts/outgrowing-django-admin.html Tue, 17 Feb 2026 00:00:00 GMT For a bit of dessert work this week, I'm working on a full-fledged attempt at replacing the majority of our stock Django admin usage with something purposeful. For a bit of dessert work this week, I'm working on a full-fledged attempt at replacing the majority of our stock Django admin usage with something purposeful.

    I say majority and not totality because even though I am an unreasonable person, I am not that unreasonable. We have over a hundred Django models, and the idea of trying to rip and replace each and every one of them — or worse yet, to design some sort of DSL by which we do that — is too quixotic even for me. The vast majority of our admin usage coalesces around three main models, and they're the ones you might guess: the user/newsletter model, the email model, and the subscriber model. My hope is that building out a markedly superior interface for interacting with these three things and sacrificing the long tail still nets out for a much happier time for myself and the support staff.

    Django admin is a source of both much convenience as much frustration: the abstractions make it powerful and cheap when you're first scaling, but the bill for those abstractions come due in difficult and intractable ways.

    When I talk with other Django developers, they divide cleanly into one of two camps: either "what are you talking about, Django admin is perfect as-is" or "oh my God, I can't believe we didn't migrate off of it sooner." Ever the annoying centrist, I find myself agreeing with both camps:

    1. Django admin is an amazing asset;
    2. I am excited to be, if not rid of it, to be seeing much less of it in the future.

    Let's set aside the visual design of the admin for a second, because arguing about visual design is not compelling prose. To me, the core issue with Django's admin interface, once you get more mature, is the fact that it's a very simple request-response lifecycle. Django pulls all the data, state, and information you might need and throws it up to a massive behemoth view for you to digest and interact with. It is by definition atomic: you are looking at a specific model, and the only way to bring in other models to the detail view is by futzing around with inlines and formsets.

    The classic thing that almost any Django developer at scale has run into is the N+1 problem — but not even necessarily the one you're thinking about. Take a fairly standard admin class:

    class EmailAdmin(admin.ModelAdmin):
        list_display = ["subject", "user", "status", "created_at"]
    

    If you've got an email admin object and one of the fields on the EmailAdmin is a user — because you want to be able to change and see which user wrote a given email — Django by default will serialize every single possible user into a nice <select> tag for you. Even if this doesn't incur a literal N+1, you're asking the backend to generate a select with thousands (or more) options; the serialization overhead alone will timeout your request. And so the answer is, nowadays, to use autocomplete_fields or raw_id_fields, which pulls in a jQuery 1.9 package Yes, in 2026. No, I don't want to talk about it. to call an Ajax endpoint instead:

    class EmailAdmin(admin.ModelAdmin):
        list_display = ["subject", "user", "status", "created_at"]
        autocomplete_fields = ["user", "newsletter"]
        list_select_related = ["user", "newsletter"]
    

    This is the kind of patch that feels like a microcosm of the whole problem: technically correct, ergonomically awkward, and aesthetically offensive.


    But the deeper issue is composability rather than performance. A well-defined data model has relationships that spread in every direction. A subscriber has Stripe subscriptions and Stripe charges. It has foreign keys onto email events and external events. When you're debugging an issue reported by a subscriber, you want to see all of these things in one place, interleaved and sorted chronologically.

    Django admin's answer to this is inlines:

    class StripeChargeInline(admin.TabularInline):
        model = StripeCharge
        extra = 0
        readonly_fields = ["amount", "created_at", "status"]
    
    class EmailEventInline(admin.TabularInline):
        model = EmailEvent
        extra = 0
        readonly_fields = ["event_type", "created_at"]
    
    class SubscriberAdmin(admin.ModelAdmin):
        inlines = [StripeChargeInline, EmailEventInline]
        list_display = ["email", "newsletter", "created_at"]
        list_select_related = ["newsletter"]
    

    This works — until it doesn't. You start to run into pagination issues; you can't interleave those components with one another because they're rendered as separate, agnostic blocks; you can't easily filter or search within a single inline. You could create a helper method on the subscriber class to sort all related events and present them as a single list, but you once again run into the non-trivial problem of this being part of a fixed request-response lifecycle. And that kind of serialized lookup can get really expensive:

    class SubscriberAdmin(admin.ModelAdmin):
        readonly_fields = ["recent_activity"]
    
        def recent_activity(self, obj):
            charges = obj.stripe_charges.order_by("-created_at")[:10]
            events = obj.email_events.order_by("-created_at")[:10]
            # Merging, sorting, rendering to HTML...
            # All of this happens synchronously, on every page load.
            combined = sorted(
                [*charges, *events],
                key=lambda x: x.created_at,
                reverse=True
            )
            return format_html_join(
                "\n",
                "<div>{} — {}</div>",
                [(item.created_at, item) for item in combined]
            )
    

    You can do more bits of cleverness — parallelizing lookups, caching aggressively, using select_related and prefetch_related everywhere — but now you're fighting the framework rather than using it. The whole point of Django admin was to not build this stuff from scratch, and yet here you are, building bespoke rendering logic inside readonly_fields callbacks.


    I still love Django admin. On the next Django project I start, I will not create a bespoke thing from day one but instead rely on my trusty, outdated friend until it's no longer bearable.

    But what grinds my gears is the fact that, as far as I can tell, every serious Django company has this problem and has had to solve it from scratch. There's no blessed graduation path, whether in the framework itself or the broader ecosystem. I think that's one of the big drawbacks of Django relative to its peer frameworks. As strong and amazing as its community is, it's missing a part of the flywheel from more mature deployments upstreaming their findings and discoveries back into the zeitgeist.

    ]]>
    Notes on "Harness Engineering" https://jmduke.com/posts/harness-engineering.html https://jmduke.com/posts/harness-engineering.html Mon, 16 Feb 2026 00:00:00 GMT I find it useful and revealing to perform very close readings of engineering blog posts from frontier labs. They seem like meaningful artifacts that, despite their novelty, are barely discussed at any level except the surface one. I find it useful and revealing to perform very close readings of engineering blog posts from frontier labs. They seem like meaningful artifacts that, despite their novelty, are barely discussed at any level except the surface one.

    I try to keep an open but keen mind when reading these posts, both trying to find things that don't make much sense when you think about them for more than a couple seconds and what things are clearly in the internal zeitgeist for these companies but haven't quite filtered themselves out into mainland. And so I read Harness Engineering with this spirit in mind. Some notes:

    1. It's disingenuous to make this kind of judgment without knowing more about the use case and purpose of the application itself, but the quantitative metrics divulged are astounding. The product discussed in this post has been around for five months. It contains over one million lines of code and is not yet ready for public consumption but has a hundred or so users. If you had told me those statistics in any other context, I would be terrified of what was happening within that poor Git repository — which is to say nothing of a very complicated stack relative to an application of that size. Why do you need all this observability for literally one hundred internal users? Again, there might be a very reasonable answer that we are not privy to.
    2. Most of the techniques discussed in this essay — like using agents.md as an index file rather than a monolith — have been fully integrated into the meta at this point. But there's one interesting bit about using the repository as the main source of truth, and in particular building a lot of tooling around things like downloading Slack discussions or other bits of exogenous data so they can be stored at a repository level. My initial reaction was one of revulsion. Again, that poor, poor Git repository. But in a world where you're optimizing for throughput, getting to eliminate network calls and MCP makes a lot of sense — though I can't help but feel like storing things as flat files as opposed to throwing it in a SQLite database or something a little bit more ergonomic would make more sense. See insourcing-your-data-warehouse.
    3. The essay hints at, but does not outright discuss, failure modes. They talk about the rough harness for continuous improvement and paying down of technical debt, as well as how to reproduce and fix bugs, but comparatively little about the circumstances by which those bugs and poor patterns are introduced in the first place.
    4. Again, I get it. It's an intellectual exercise, and I'm certainly not one to suggest that human-written code is immune from bugs and poor abstractions. But this does feel a little bit like Synecdoche, New York — an intellectual meta-exercise that demands just as much attention and care and fostering as the real thing. At which point one must ask themselves: why bother?

    This was a fairly negative list of notes, and I want to end with something positive: I do generally agree with the thrust of the thesis. Ryan writes:

    This is the kind of architecture you usually postpone until you have hundreds of engineers. With coding agents, it's an early prerequisite: the constraints are what allows speed without decay or architectural drift.

    I think this is absolutely the right mindset. Build for developer productivity as if you have one more order of magnitude of engineers than you actually do.

    ]]>
    Heaven or Las Vegas https://jmduke.com/posts/heaven-or-las-vegas.html https://jmduke.com/posts/heaven-or-las-vegas.html Mon, 16 Feb 2026 00:00:00 GMT I&#39;m not sure how to politely accuse the entire genre of dreampop of having ripped off this album in particular. It is perhaps the recency bias. I think of Hitchcock&#39;s Rope when I say that this album, rather, just like that film, could have been made tomorrow and would be held as simultaneously fresh and timeless. There&#39;s an, if you&#39;ll excuse the cliché, dreaminess and mist to these tracks. Unlike, say, Midnight City, an album that I do love regardless, never feels manufactured or... I'm not sure how to politely accuse the entire genre of dreampop of having ripped off this album in particular. It is perhaps the recency bias. I think of Hitchcock's Rope when I say that this album, rather, just like that film, could have been made tomorrow and would be held as simultaneously fresh and timeless. There's an, if you'll excuse the cliché, dreaminess and mist to these tracks. Unlike, say, Midnight City, an album that I do love regardless, never feels manufactured or as if it's running out of steam. A perfect album that I am hitting myself for never listening to sooner.

    ]]>
    The Rip https://jmduke.com/posts/the-rip.html https://jmduke.com/posts/the-rip.html Mon, 16 Feb 2026 00:00:00 GMT The Rip is a deeply simulacric movie, not just in the Netflix action thriller sense, but also in the sense of it largely existing in the context of all the cliches, a word I do not deploy derogatorily (yet), in front of it: The Rip is a deeply simulacric movie, not just in the Netflix action thriller sense, but also in the sense of it largely existing in the context of all the cliches, a word I do not deploy derogatorily (yet), in front of it:

    1. our understanding as filmgoers of Matt and Ben's dynamic,
    2. our understanding of every single Miami drug film that came before it,
    3. our understanding of the way these potboiler films in general were.

    It is a deeply derivative film, but does us the mercy of being derivative in occasionally interesting ways.

    Affleck's performance is, I think, the weaker of the two, playing what oftentimes is a caricature of John Bernthal's shtick in We Own This City. Damon does a good job imbuing the film with wit and a sense of ambiguity, and his performance of a man who only barely has the situation under control is the bright spot — especially in contrast to the rest of his cast, who are very excited to collect that Netflix money and move on.

    The third act of the film lays everything bare and throws into sharp relief the weaknesses of Netflix's production process. You could otherwise almost have multiple characters explain the plot of the film that you've just watched in its entirety. And if that weren't enough, you get gratuitous blown-up flashbacks to scenes you've already seen 30 minutes prior to drive the point home. Not every film needs to be awash in subtext, but there's something so aggressive about the way this film is not content until it goes around in a circle as every actor says exactly what their intentions are and exactly what had happened, just in case you were too busy folding laundry in the other room to really pick up on it.

    Combined with an anarchic and pointless 10-minute action scene to cap the movie off, it degraded from enjoyable pablum to actively being a bummer of a watch. You could have created a version of this movie, maybe 20 years ago, that was not exactly a masterpiece but had a soul and a rightful cult following in the same way Michael Mann's films do, despite their flaws.

    Maybe that's the movie Damon and Affleck wanted to make, but it's not the one they made. Instead, we are left with The Rip.

    ]]>
    The death of software, the A24 of software https://jmduke.com/posts/death-of-software.html https://jmduke.com/posts/death-of-software.html Sat, 14 Feb 2026 00:00:00 GMT Steven Sinofsky recently published Death of Software. Nah., arguing via historical case studies that AI will not kill software any more than previous technological shifts killed their respective incumbents. I agree with the headline thesis. But I think his media analogy deserves a sharper look, because it actually complicates his optimism in ways worth taking seriously. Steven Sinofsky recently published Death of Software. Nah., arguing via historical case studies that AI will not kill software any more than previous technological shifts killed their respective incumbents. I agree with the headline thesis. But I think his media analogy deserves a sharper look, because it actually complicates his optimism in ways worth taking seriously.


    He writes that there is "vastly more media today than there was 25 years ago," pointing to streaming as evidence that disruption creates abundance rather than destruction. This is telling, because I agree with both sides of the glass:

    1. There will be more software, not less, in the future.
    2. The quality of that software — as defined by the heuristics of yesteryear — will be lower.

    The shift to streaming has not killed media. But it has, to put it mildly, made the aggregate quality of the product worse, and in doing so shifted the value generated away from creative labor and towards platforms and capital. Warner Bros. is, to hear some people say it, the last great conventional studio producing consistently risky and high-quality work that advances the medium forward; Netflix, Apple, et al do put out some extremely great stuff, but the vast majority of their budget goes to things like Red Notice — films designed with their audiences' revealed preferences (i.e., browsing their phone while the film is on) in mind.


    And yet! The greatest studio of the past decade was also a studio founded in, essentially, the past decade — A24, in 2012. I think it's uncontroversial to say that no other studio has had a higher batting average, and they've done it the right way: very pro-auteur, very fiscally disciplined, focusing more on an overall portfolio brand and strong relationships than the need for Yet Another Tentpole Franchise.

    A24 didn't succeed despite the streaming era — they succeeded because of it. The explosion of mediocre content created a vacuum for taste, for curation, for a brand that stood for something. When everything is abundant and most of it is forgettable, the scarce thing is discernment.

    The interesting question isn't "will there be more software?" — it's who captures the value, and what excellence looks like in a world of abundance.

    (Kicker: A24 just took a round of additional funding from Thrive Capital last year. The market, it seems, agrees.)

    ]]>
    Better tests https://jmduke.com/posts/good-tests.html https://jmduke.com/posts/good-tests.html Sat, 14 Feb 2026 00:00:00 GMT What makes for a good test? I feel like there is a dearth of useful literature on this subject, perhaps because a lot of the content which ostensibly aims to answer it ends up going down one of the myriad rabbit holes that people who are passionate about testing often go down — whether it&#39;s TDD or BDD or property-based testing or whatever. I don&#39;t want to do that. I want to write a few bullet points instead. What makes for a good test? I feel like there is a dearth of useful literature on this subject, perhaps because a lot of the content which ostensibly aims to answer it ends up going down one of the myriad rabbit holes that people who are passionate about testing often go down — whether it's TDD or BDD or property-based testing or whatever. I don't want to do that. I want to write a few bullet points instead.

    The target audience: junior engineers who understand the why and the what of tests, but have not seen enough of them in mature engineering organizations to really grasp the how.


    Tests should be as short as possible

    More specifically: tests should be as short as possible relative to the uniqueness of the behavior they're trying to test.

    Let me give you a simple example. Say we want to test that patching Buttondown's API with an empty subject fails if and only if the status of the email is not draft. Let's pretend we haven't written any tests for this API before. You start off by catching the base case:

    def test_patching_email_with_empty_subject_fails_for_sent_email():
        user = User.objects.create(username="test")
        api_key = APIKey.objects.create(user=user)
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION=f"Token {api_key.key}")
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=EmailStatus.SENT,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == 400
    

    Then your coworker mentions that this test would still pass if you always threw that error — you need to verify the draft case too. So you copy and paste it, change the status, and check the other side of the boundary:

    def test_patching_email_with_empty_subject_succeeds_for_draft_email():
        user = User.objects.create(username="test")
        api_key = APIKey.objects.create(user=user)
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION=f"Token {api_key.key}")
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=EmailStatus.DRAFT,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == 200
    

    Maybe you add two more for SENDING and IMPORTED to cover all the bases. Now you have four near-identical tests. I purport that this is bad — for two reasons that map to two audiences:

    1. The reader. At some point, someone will introduce a change that causes one of these tests to fail. They will begin by reading the test in full — or, I suppose, asking their LLM to do the same — at which point they will have to spend at least one nanosecond parsing out the client and authentication logic to figure out whether it's germane to the behavior at hand. Obviously, this is not the end of the world. But nanoseconds are important. And while this use case is trivial, it becomes much less so when the banal setup goes from four lines of code to twenty.
    2. The writer. I'm rounding up a little bit here when I say that literally every software developer in the world starts writing a test by copying and pasting the closest one to it. When you open this file and see that every single test starts with the same setup, you therefore assume that all subsequent tests should too — either out of laziness or of misplaced virtue. And now the problem persists. And when you end up changing a bit of that authentication logic, you have to change it in every single test.

    Your testing framework might have an abstraction tailor-made for this. I'm fond of pytest, which uses fixtures — just pull the common functionality out and let the framework inject it:

    @pytest.fixture
    def authenticated_client():
        user = User.objects.create(username="test")
        api_key = APIKey.objects.create(user=user)
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION=f"Token {api_key.key}")
        return client, user
    

    Now that original test collapses down to just the behavior under test — the fixture name in the function signature is all pytest needs to wire it up:

    def test_patching_email_with_empty_subject_fails_for_sent_email(
        authenticated_client,
    ):
        client, user = authenticated_client
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=EmailStatus.SENT,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == 400
    

    Better! But we can go further. A pattern that I'm particularly fond of is parameterization, in which you create a metatest and pass in a bunch of various inputs and outputs to isolate the change as specifically as possible:

    @pytest.mark.parametrize(
        "status, expected_code",
        [
            (EmailStatus.DRAFT, 200),
            (EmailStatus.SENT, 400),
            (EmailStatus.SENDING, 400),
            (EmailStatus.IMPORTED, 400),
        ],
    )
    def test_patching_email_with_empty_subject(
        authenticated_client, status, expected_code
    ):
        client, user = authenticated_client
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=status,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == expected_code
    

    Now the reader can see, at a glance, exactly what varies and exactly what doesn't. The authentication is someone else's problem. The behavioral boundary — the thing we actually care about — is front and center.


    Use snapshots for full-context assertions

    Snapshots are increasingly in vogue as a way to, rather than assert a characteristic about a response, assert the entire response, and then fail the test on any change to that overall response. Snapshot libraries also come bundled with tooling around reviewing and regenerating these snapshots.

    These are nice, but not wholly necessary for every situation. The main idea is understanding the full context of behavior under assertion. Snapshot tests are great for things where looking at just one facet of an output is necessary but insufficient for understanding the entirety of the behavior. Consider the difference between:

    def test_empty_subject_error(authenticated_client):
        client, user = authenticated_client
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=EmailStatus.SENT,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == 400
        assert response.json()["code"] == "invalid_subject"
    

    This tells you the endpoint returns a 400 with the right error code. But it tells you nothing about the rest of the response — the human-readable message, the field name, the error structure. If someone changes the error message from "Subject cannot be empty for sent emails" to "lol no", this test still passes. Compare:

    def test_empty_subject_error(authenticated_client, snapshot):
        client, user = authenticated_client
        email = Email.objects.create(
            owner=user,
            subject="Hello",
            status=EmailStatus.SENT,
        )
        response = client.patch(
            f"/api/v1/emails/{email.id}",
            {"subject": ""},
        )
        assert response.status_code == 400
        assert response.json() == snapshot
    

    Now the snapshot file captures the entire JSON blob — the code, the message, the field, the structure — and any change to any of it will surface in your diff. API responses, rendered HTML, and standalone components are all great candidates for snapshot testing.


    Only test meaningful differences

    Referring back to the example I led with: if you fed that behavior to an LLM and asked it to write some tests, chances are it would not just create the four tests that we outlined — it would also create a test for a status of IN_FLIGHT, a test for a status of IMPORTED, and maybe a test to see if the behavior remains the same when you also pass a body alongside the empty subject:

    # Please don't do this.
    @pytest.mark.parametrize(
        "status, subject, body, expected_code",
        [
            (EmailStatus.DRAFT, "", None, 200),
            (EmailStatus.DRAFT, "", "Some body", 200),
            (EmailStatus.DRAFT, "Hi", None, 200),
            (EmailStatus.DRAFT, "Hi", "Some body", 200),
            (EmailStatus.SENT, "", None, 400),
            (EmailStatus.SENT, "", "Some body", 400),
            (EmailStatus.SENT, "Hi", None, 200),
            (EmailStatus.SENDING, "", None, 400),
            (EmailStatus.SENDING, "", "Some body", 400),
            (EmailStatus.IMPORTED, "", None, 400),
            (EmailStatus.IMPORTED, "", "Some body", 400),
            # ... and on and on and on ...
        ],
    )
    def test_patching_email(
        authenticated_client, status, subject, body, expected_code
    ):
        ...
    

    This frankly drives me nuts, because it adds noise and volume to your test suite without any real value. If you were to break the behavior in a subsequent commit, you'd suddenly see a dozen tests fail instead of one — and the signal-to-noise ratio of your failure output craters accordingly.

    The minimum number of test cases that truly explore the boundary space of the behavior is a good ideal to strive towards. The best PRs are ones that are bug fixes with exactly one regression test that fails beforehand and succeeds afterwards.


    Tests should look good

    Aesthetics of code is obviously subjective. But I don't think it's controversial to say that some code is messier than others, even in things that don't necessarily violate principles of code organization. Let me pick on Python by offering an example.

    Django's default test runner, like Python's unittest, uses — for some bizarre reason — Java-style camelCase assertions:

    class TestEmail(TestCase):
        def test_something(self):
            self.assertEqual(response.status_code, 200)
            self.assertIsNone(email.subject)
            self.assertContains(response, "Hello")
            self.assertQuerysetEqual(
                Email.objects.all(),
                [self.email],
            )
    

    This drives me nuts. I think it is actively painful to read assertQuerysetEqual in the context of a Python codebase. Instead, pytest just offers simple, bare assertions that are exactly as flexible as you would expect them to be:

    def test_something():
        assert response.status_code == 200
        assert email.subject is None
        assert "Hello" in response.content.decode()
        assert list(Email.objects.all()) == [email]
    

    It's useful to take a little bit of initial time to make sure your tests are clean, because whatever conventions and prior art you establish get calcified into a suite very, very quickly. A junior engineer's first test will look like whatever test they copied — and that's not a flaw in the junior engineer, it's a feature of how codebases work. Make sure the thing they're copying is worth copying.


    I know that it's increasingly en vogue to treat your codebase with a hint of nihilism — a sense that every line is, with enough patience and GPUs, destined to be subsumed into the void. I encourage you to resist that urge, if only for selfish reasons. The speed and efficacy of your application is going to be increasingly capped not by the quality of your implementation code, but by the quality of your verification code.

    ]]>
    Notes on "How I'd Grow Buttondown" https://jmduke.com/posts/how-id-grow-buttondown.html https://jmduke.com/posts/how-id-grow-buttondown.html Fri, 13 Feb 2026 00:00:00 GMT A friend sent me Andrea Bosoni&#39;s How I&#39;d grow Buttondown, a great and thoughtful piece. It&#39;s fun to read someone else do the exact intellectual exercise you&#39;ve been doing yourself for years, and seeing it from the outside was genuinely useful. A friend sent me Andrea Bosoni's How I'd grow Buttondown, a great and thoughtful piece. It's fun to read someone else do the exact intellectual exercise you've been doing yourself for years, and seeing it from the outside was genuinely useful.

    Props to Andrea: the advice is, on the whole, well-considered and reasonable, and I wanted to offer some responses — less to rebut the points and more to characterize them in a more interesting light.

    Lean into the demo splash

    This is a good idea and one that's been increasingly en vogue. Cursor's landing page is a great example of this pattern done well. And we do plaster links to our demo site throughout blog posts and documentation for exactly this reason.

    That being said! An initial A/B test of running our own landing page in this manner showed a pretty bad conversion rate. This was an older, less polished version of our demo site — which has grown substantially more robust in terms of fixture data since then — so it's worth rerunning. But it's a good example of how sometimes intuition gets punched in the face by data. You might be curious about why it didn't perform as well. I am too. My suspicion is that it simply couldn't compete with our prose-heavy positioning copy, which we've iterated and honed pretty sharply over the past few years.

    "Emails" is a tough word to sell

    The author correctly identifies this, and to a certain extent it's a broader existential crisis we have. If you say newsletters, it's a very specific word that works for some of our customer cohorts but turns off our business users. If you say campaigns, like Mailchimp does, you alienate the prosumer and casual customer cohort — which is our largest by overall volume. In reality, I don't think a lot of people see that H1 tag and are confused unless they're coming to Buttondown from some completely unrelated context, like Hacker News. (See those comments.) Nouns are tricky, though.

    Compete pages

    Like a lot of programmatic SEO work, these can kind of feel like the simulacra of smart marketing as opposed to the real thing. Our compete pages drive, at this point, very little volume compared to our truer pillar pages — pricing and features. We should invest more in them, sure, but the reality is that they're already richer than 75% of the world's compete pages, and despite that, they drive very little intentional traffic. The juice-to-squeeze ratio is not what you'd hope.

    Domain versus subdomain

    This is true for a number of reasons, both technical and otherwise. But if we were to ever do a switchover, we wouldn't do it for SEO or marketing reasons — let alone as an experiment — because it's a very serious engineering undertaking, and it's hard to justify very serious engineering undertakings on a hunch that it might have a knock-on effect on search rankings.

    Zooming out

    All of these are fairly minor points, though. Here's the big one, as it pertains to Buttondown's growth and how I think about marketing writ large.

    From both onboarding surveys and overall quantitative traffic: half of our customers come through word of mouth, one-fourth through search, and one-fourth through LLM-based traffic. (I'll talk more about that last bucket in a later post.) When you divide that not by overall customer count but by revenue, it's two-thirds word of mouth and one-third everything else.

    This is what used to be referred to in rhapsodizing tones as product-led growth. And it's a lovely little coping mechanism that lets us justify spending the vast majority of our time making a better product and thereby attracting more users. Where this approach falters — and this is the thing I think about most — is trying to expand out of our current orbit of customers to new verticals and cohorts.

    Which is to say: most of the advice in Andrea's post is correct and reasonable, and we should do more of it. The reason we don't is not ignorance but prioritization — the ROI on product improvement, for Buttondown's current stage, dwarfs the ROI on marketing optimization. That calculus may change, but to a certain extent we don't want it to — the unit economics and incentives are very clean this way.

    ]]>
    One status field per model https://jmduke.com/posts/one-status-field-per-model.html https://jmduke.com/posts/one-status-field-per-model.html Thu, 12 Feb 2026 00:00:00 GMT Any sufficiently old application starts to succumb to a pernicious form of technical debt known in street parlance as shitty data modeling. Any sufficiently old application starts to succumb to a pernicious form of technical debt known in street parlance as shitty data modeling.

    Sometimes this manifests as the god object: a single model that represents the user, and the settings, and the DNS configuration, and twelve other things. Sometimes this comes in the form of a table (or multiple tables) where the initial set of data modeling concerns in the early goings of the project don't quite match the reality discovered along the way, and a series of subtle mismatches collide with each other in the same way that subtle mismatches between tectonic plates do.

    Data models, unlike other areas of tech debt, are correctly scary to refactor. Even in Django — an application framework with really robust, mature migration tooling — reshaping data in production is non-trivial. The weight associated with even relatively simple schema changes can be so overwhelming as to forever dissuade a would-be re-architect from making things right.

    Therefore, it is that much more important to spend the extra mental energy early on to make sure, whenever possible, your data model is a roughly correct one, and to course correct early when it isn't.

    There are many ways to do this, and the goal of describing a virtuous data model in its entirety is too large and broad a problem for this measly little essay. Instead, I want to share a heuristic that I have found particularly useful — one which is summed up, as many of my blog posts are, in the title.

    Every data model must have at most one status field.

    If you're thinking about making a change such that a model has more than one status field, you have the wrong data model.


    Let me illustrate via self-flagellation and talk about Buttondown's own problematic model: the Newsletter object.

    The Newsletter object has three status fields within its lush, expansive confines:

    1. A status field (the normal one)
    2. A sending_domain_status field
    3. A hosting_domain_status field

    This is incorrect. We should have created standalone models for the sending domain and hosting domain, each with a simple status field of its own, and drawn foreign keys from the Newsletter onto those. We did not do this, because at the time it felt like overkill.

    And so. You pay the price — not in any one specific bug, but in weirdness, in the difficulty of reasoning about the code. Is there a meaningful difference between an active status and a hosting_domain_status of active for an active newsletter, versus an active status and a hosting_domain_status of pending? What queries should return which combinations? The confusion compounds.

    Again, I know this sounds trivial. But every good data model has syntactic sugar around the state machine, and every good state machine has a unary representation of its state. See also: enums.

    ]]>
    RIP XSLT https://jmduke.com/posts/styled-rss-feeds.html https://jmduke.com/posts/styled-rss-feeds.html Wed, 11 Feb 2026 00:00:00 GMT If you visit feed.xml in your browser, you will see that I have done the Cool Kid thing and added styling to it via XSLT. This was originally something I&#39;ve wanted to do for years — both for my own blog and for Buttondown — and I finally got around to it, only to immediately learn that XSLT support is being deprecated. If you visit feed.xml in your browser, you will see that I have done the Cool Kid thing and added styling to it via XSLT. This was originally something I've wanted to do for years — both for my own blog and for Buttondown — and I finally got around to it, only to immediately learn that XSLT support is being deprecated.

    I can't even be particularly upset about the decision. It makes sense and is entirely rational — the security story around libxslt is grim, and if only 0.02% of page loads use XSLT, the cost of maintaining it outweighs the benefit. And yet. It is nonetheless a bummer.

    So enjoy those beautiful RSS feeds until November 17, 2026, when they go away forever. Or perhaps I will go with the polyfill option suggested by Chrome to hold on to them for a little bit longer — until that too is inevitably deprecated.


    P.S. Christian shared with me XSLT.rip, an absolutely terrific page devoted to this exact quandary.

    ]]>