Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
3
When we added support for complex filtering in Buttondown, I spent a long time trying to come up with a schema for filters that felt sufficiently ergonomic and future-proof. I had a few constraints, all of which were reasonable: It needed to be JSON-serializable, and trivially parsable by both the front-end and back-end. It needed to be arbitrarily extendible across a number of domains (you could filter subscribers, but also you might want to filter emails or other models.) It needed to be able to handle both and and or logic (folks tagged foo and bar as well as folded tagged foo or bar). It needed to handle nested logic (folks tagged foo and folks tagged bar or baz.) The solution I landed upon is not, I’m sure, a novel one, but googling “recursive filter schema” was unsuccessful and I am really happy with the result so here it is in case you need something like this: @dataclass class FilterGroup: filters: list[Filter] groups: list[FilterGroup] predicate: "and" | "or" ...
3 days ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Applied Cartography

Does that dependency spark joy?

If there's been one through line in changes to Buttondown's architecture over the past six months or so, it's been the removal and consolidation of dependencies: on the front-end, back-end, and in paid services. I built our own very spartan version of Metabase, Notion, and Storybook; we vended a half-dozen or so Django packages that were not worth the overhead of pulling from PyPI (and rewrote another half-dozen or so, which we will open-source in due time); we ripped out c3, our visualization library, and built our own; we ripped out vuedraggable and a headlessui and a slew more of otherwise-underwhelming frontend packages in favor of purpose-built (faster, smaller, less-flexible) versions. [1] There are a few reasons for this: Both Buttondown as an application and I as a developer have now been around long enough to be scarred by big ecosystem changes. Python has gone through both the 2.x to 3.x transition and, more recently, the untyped to typed transition; Vue has gone from 2.x to 3.x. The academic problem of "what happens if this language completely changes?" is no longer academic, and packages that we installed back in 2018 slowly succumbed to bitrot. It's more obvious to me now than a few years ago that pulling in dependencies incurs a non-trivial learning cost for folks paratrooping into the codebase. A wrapper library around fetch might be marginally easier to invoke once you get used to it, but it's a meaningful bump in the learning curve to adapt to it for the first time. It is easier than ever to build 60% of a tool, which is problematic in many respects but useful if you know exactly which 60% you care about. (Internal tools like Storybook or Metabase are great examples of this. It was a fun and trivial exercise to get Claude to build a tool that did everything I wanted Metabase to do, and save me $120/mo in the process.) We still use a lot of very heavy, very complex stuff that we're very happy with. Our editor sits on top of tiptap (and therefore ProseMirror); we use marked and turndown liberally, because they're fast and robust. On the Python side, our number of non-infrastructural packages is smaller but still meaningful (beautifulsoup, for instance, and django-allauth / django-anymail which are both worth their weight in gold). But the bar for pulling in a small dependency is much higher than it was, say, twelve months ago. My current white whale is to finally get rid of axios. 39 call sites to go! ↩︎

a week ago 7 votes
HQ1

After many wonderful years of working out of my home office (see Workspaces), I've now "expanded" [1] into an office of my own. 406 W Franklin St #201 is now the Richmond-area headquarters of Buttondown. Send me gifts! The move is a bittersweet one; it was a great joy to be so close to Haley and Lucy (and, of course, Telly), and the flexibility of being able to hop off a call and then take the dog for a walk or hold Lucy for a while was very, very nice. At the same time, for the first time in my life that flexibility has become a little bit of a burden! It turns out it is very hard to concentrate on responding to emails when your alternative is to play with your daughter giggling in the adjoining room; similarly, as Buttondown grows and as more and more of my time is spent on calls, it turns out long-winded demos and onboarding calls are logistically trickier when it is Nap Time a scant six feet away. And, beyond that, it's felt harder and harder to turn my brain off for the day: when there is always more work to be done, it's hard not to poke away at a stubborn pull request or jot down some strategy notes instead of being more present for my family (or even for myself, in a non-work capacity.) So, I leased an office. The space is pretty cool: it's downtown in the sweet spot of a little more than a mile away from the house: trivially walkable (or bikeable, as the above photo suggests) but far enough away to give me a good bit of mental space. The building is an old manor (turned dormitory, turned office building). I've got a bay window with plenty of light but no views; I've got a nice ethernet connection and a Mac Mini with very few things installed; I've got a big Ikea desk and a printer; I've got an alarm on my phone for 4:50pm, informing me that it's time to go home, where my world becomes once again lively and lovely, full of noise and joy and laughter. Air quote because I'm fairly confident this office is actually smaller than the home office. ↩︎

a week ago 8 votes
Naughty vs. nice

I love this bit from Paul Graham on pattern-matching founders: Though the most successful founders are usually good people, they tend to have a piratical gleam in their eye. They're not Goody Two-Shoes type good. Morally, they care about getting the big questions right, but not about observing proprieties. That's why I'd use the word naughty rather than evil. They delight in breaking rules, but not rules that matter. This quality may be redundant though; it may be implied by imagination. I love this not because I agree with the sentiment, and in fact I think you can point to a lot of Icarian tendencies (and perhaps pervasive industry-wide rot) as germinating in this naughtiness, but because it is a specific and opinionated characteristic — as opposed to, like, "determined!" and "smart!" and "driven!" It's novel, it's a characteristic with a viewpoint around which reasonable people can agree/disagree. Antimetal is not a YC company, but it certainly embodies naughtiness. Its founder made a large hullabaloo about trying to commission "the highest quality publicly available version" of the Facebook Red Book, but of course couldn't resist a tiny act of digital vandalism by inserting its own branding into the scan. Does this matter, on a grand scale? Is this an evil act? Probably not, but certainly a naughty one. Buttondown in 2025 has reached a sort of escape velocity, less in terms of growth per ce (though also that!) and more in terms of the median user being very far way from my orbit: these users are less technical and more wary than the ones I am used to onboarding. Users of new tools — especially tools that must be entrusted with important data — are wary these days. They're wary of pivots to video, of shifting business models and sudden price hikes and emails announcing that the curtain is coming down this time next week. It is unfortunate that this wariness — a kind of cynicism — is not only pervasive but entirely rational. Anyone who has used anything new over the past few years has a high number of since-shuttered apps that they trusted with their time and money and data and energy, only to be rewarded with an "Our Incredible Journey" email. At a high level, I think this stems from the same vein as naughtiness: a tendency to think of systems and expectations as something to be overcome, as social contracts as a thing to be voided or ignored rather than bolstered. We get a lot of questions that boil down to "why should I trust [Buttondown]?" The blithe answer — the one that I generally try not to give, even though I think it's the most rigorous and correct one — is that, well, you shouldn't — insofaras you should only trust any company as much as you can exfiltrate your data. We've made a lot of decisions in service of decades-long continuity; we're cash-flow positive, we're stable and robust; our incentives are aligned with yours. But, more than that, the email space is novel in that you can always pack up your entire dataset — archives, addresses, et al — and ship them off to a competitor. You shouldn't need to trust us; you should find us valuable enough to be worth keeping around. Email is also unique in that it's, by software standards, a very mature industry — one with a long history already. Many of my customers come with data exports from tools that they started using fifteen years ago; prospects who I reached out to in 2019 follow up in 2025. After twelve months of active usage, we ask every paying customer a single question: "why are you still using Buttondown?" [1] There are two answers whose volume dwarf the rest: Because the customer support is really good. Because I haven't had an experience that has prompted me to look elsewhere. Customer goodwill is a real asset; it is one that will probably become more valuable over the next decade, as other software-shaped assets start to become devalued. It feels almost anodyne to say "it is in a company's best interest to do right by their customers", but our low churn and high unpaid growth in a space uniquely defined by lack of vendor lock-in is perhaps a sign that being nice is an undervalued strategy. And "being nice" in a meaningful sense is, like "being naughty", something that gets baked into an organization's culture very early and very deeply. The implicit subtext being "...given that you can, in an afternoon's work, migrate to a competitor, most of whom are substantially less expensive." ↩︎

2 weeks ago 12 votes
YOLO-squashing our Django repository

Buttondown's core application is a Django app, and a fairly long-lived one at that — it was, until recently, sporting around seven hundred migration files (five hundred of which were in emails, the "main" module of the app). An engineer pointed out that the majority of our five minute backend test suite was spent not even running the tests but just setting up the database and running all of these migrations in parallel. I had been procrastinating squashing migrations for a while; the last time I did so was around two years ago, when I was being careful to the point of agony by using the official squash tooling offered by Django. Django's official squashing mechanism is clever, but tends to fall down when you have cross-module dependencies, and I lost an entire afternoon to trying to massage things into a workable state. This time, I went with a different tactic: just delete the damn things and start over. (This is something that is inconsiderate if you have lots of folks working on the codebase or you're letting folks self-host the codebase; neither of these apply to us.) rm rf **/migrations/* worked well for speeding up the test suite, but it was insufficient for actually handling things in production. For this, I borrowed a snippet from django-zero-migrations (a library around essentially the same concept): from django.core.management import call_command from django.db.migrations.recorder import MigrationRecorder MigrationRecorder.Migration.objects.all().delete() call_command("migrate", fake=True) And voila. No fuss, no downtime. Deployments are faster; CI is much faster; the codebase is 24K lines lighter. There was no second shoe. If you were like me 24 hours ago, trying to find some vague permission from a stranger to do this the janky way: consider the permission granted. Just take a snapshot of your database beforehand just in case, and rimraf away.

2 weeks ago 13 votes

More in technology

Humanities Crash Course Week 10: Greek Drama

Week 10 of the humanities crash course had me reading (and listening to) classic Greek plays. I also listened to the blues and watched a movie starring a venerable recently departed actor. How do they connect? Perhaps they don’t. Let’s find out. Readings The plan for this week included six classic Greek tragedies and one comedy: Sophocles’s Oedipus Rex, Oedipus at Colonus, and Antigone, Aeschylus’s Agamemnon, Euripides’s The Bacchae, and Aristophanes’s Lysistrata. The tragedies by Sophocles form a trilogy. Oedipus Rex is by far the most famous: the titular character discovers he’s not just responsible for his father’s death, but inadvertently married his widowed mother in its wake. Much sadness ensues. The other two plays continue the story. Oedipus at Colonus has him and his daughters seeking protection in a foreign land as his sons duke it out over his throne. In Antigone, Oedipus’s daughter faces the consequences of burying her brother after his demise in that struggle. In both plays, sadness ensues. Agamemnon dramatizes a story we’ve already encountered in the Odyssey: the titular king returns home only to be betrayed and murdered by his wife and her lover. The motive? The usual: revenge, lust, power. Sadness ensues. The Bacchae centers on the cult of the demigod Dionysus. He comes to Thebes to avenge a slanderous rumor and spread his own cult. Not recognizing him, King Pentheus arrests him and persecutes his followers, a group of women that includes Pentheus’s mother, Agave. In ecstatic frenzy, Agave and the women tear him apart. Again, not light fare. Lysistrata, a comedy, was a respite. Looking to end to the Peloponnesian War, a group of women led by the titular character convince Greek women to go on a sex strike until the men stop the fighting. For such an old play, it’s surprisingly funny. (More on this below.) These plays are very famous, but I’d never read them. This time, I heard dramatizations of Sophocles’s plays and an audiobook of The Bacchae, and read ebooks of the remaining two. The dramatizations were the most powerful and understandable, but reading Lysistrata helped me appreciate the puns. Audiovisual Music: Gioia recommended classic blues tunes. I listened to Apple Music collections for Blind Lemon Jefferson and Blind Willie Johnson. I also revisited an album of blues music compiled for Martin Scorcese’s film series, The Blues. My favorite track here is Lead Belly’s C.C. Rider, a song that’s lived rent free in my brain the last several days: Art: Gioia recommended looking at Greek pottery. I studied some of this in college and didn’t spend much time looking again. Cinema: rather than something related to the readings, I sought out a movie starring Gene Hackman, who died a couple of weeks ago. I opted for Francis Ford Coppola’s THE CONVERSATION, which is about the ethics of privacy-invading technologies. Even though the movie is fifty-one years old, that description should make it clear that it’s highly relevant today. Reflections I was surprised by the freshness of the plays. Yes, most namechecks are meaningless without notes. (That’s an advantage books have over audiobooks.) But the stories deal with timeless themes: truth-seeking, repression, free will vs. predestination, the influence of religious belief on our actions, relations between the sexes, etc. Unsurprisingly, some of these themes are also central to THE CONVERSATION. I sensed parallels between Oedipus and the film’s protagonist, Harry Caul. ChatGPT provided useful insights. (Spoilers here for both the play and movie – but c’mon, these are old works!) Both characters investigate the truth only to find painful revelations about themselves. Both believe that gaining knowledge will help them control events – but their efforts only lead to self-destruction. Both misunderstand key pieces of evidence. Both end up “isolated, ruined by their own knowledge, and stripped of their former identity.” (I liked how ChatGPT phrased this!) Both stories explore the limits of perception: it’s possible to see (and record) and remain ignorant of the truth. Heavy stuff – as is wont in drama. Bur for me, the bigger surprise in exploring these works was Lysistrata. Humor is highly contextual: even contemporary stuff doesn’t play well across cultures. But this ancient Greek play is filled with randy situations and double entendres that are still funny. Much rides on the translation. The edition I read was translated by Jack Lindsay, and I marveled at his skills. It must’ve been challenging to get the rhymes and puns in and still make the story work. A note in the text mentioned that the Spartans in the story were translated to sound like Scots to make them relatable to the intended English audience. (!) Obviously, none of these ancient texts I’ve been reading were written in English. That will change in the latter stages of the course. I’m wondering if I should read texts originally written in Spanish and Italian in those languages, since I can. (But what would that do to my notes and running interactions with the LLMs? It’s an opportunity to explore…) Notes on Note-taking Part of why I’m undertaking this course is to experiment with note-taking and LLMs. This week, I tried a few new things. First, before reading each play, I read through its synopsis in Wikipedia. This helped me understand the narrative thread and themes and generally get oriented in unfamiliar terrain. Second, I tried a new cadence for capturing notes. These are short plays; I read one per day. (Except The Bacchae, which I read over two days.) During my early morning journaling sessions, I wrote down a synopsis of the play I’d read the previous day. Then, I asked GPT-4o for comments on the synopsis. The LLM invariably pointed out important things I’d missed. The point wasn’t making more complete notes, but helping me understand and remember better by writing down my fresh memories and reviewing them through a “third party.” I was forced to be clear and complete, since I knew I’d be asking for feedback. Third, I added new sections to my notes for each work. After the synopsis, I asked GPT-4o for an outline explaining why the work is considered important. I read these outlines and reflected on them. Then, I asked for criticisms, both modern and contemporary, that could be leveled against these works. Frankly, this is risky. One of my guidelines has been to stick to prompts where I can verify the LLM’s output. If I ask for a summary of a work I’ve just read, I’ll have a better shot at knowing whether the LLM is hallucinating. But in this case, I’m asking for stuff that I won’t be able to validate. Still, I’m not using these prompts to generate authoritative texts. Instead, the answers help me consider the work from different perspectives. The LLM helps me step outside my experience – and that’s one of the reasons for studying the humanities. Up Next Gioia scheduled Marcus Aurelius and Epictetus for week 11. I’ve read Meditations twice and loved it, and will revisit it now more systemically. But since I’m already familiar with this work, I’ll also spend more time with the Bible – the Book of Job, in particular. In addition to Job itself, I plan to read Mark Larrimore’s The Book of Job: A Biography, which explores its background. It’ll be the first time in the course that I read a work about a work. (As you may surmise, I’m keen on Job.) This will also be the first physical book I read in the course. Otherwise, I’m sticking with Gioia’s recommendations. Check out his post for the full syllabus. Again, there’s a YouTube playlist for the videos I’m sharing here. I’m also sharing these posts via Substack if you’d like to subscribe and comment. See you next week!

7 hours ago 1 votes
+ iPhone 16e review in progress: battery life

You can never do too much battery testing, but after a week with this phone I've got some impressions to share.

yesterday 2 votes
Real WordPress Security

One thing you’ll see on every host that offers WordPress is claims about how secure they are, however they don’t put their money where their mouth is. When you dig deeper, if your site actually gets hacked they’ll hit you with remediation fees that can go from hundreds to thousands of dollars. They may try … Continue reading Real WordPress Security →

yesterday 1 votes
Odds and Ends #61: Fake Woolly Mammoths

Plus why intelligence is social, Land Registry open data, and some completely invisible VFX

2 days ago 2 votes
Watch me write a task manager in 30 minutes

A core tenet of A Better Computer is showing, not telling. I don’t use a lot of press kit material or talking points from companies in my videos because I don’t particularly care about those. My incentives are fully aligned with showing software (and sometimes hardware)

2 days ago 2 votes