Why I Pay for Figma
I grew up being told to avoid spending money whenever possible, and that a poor craftsman blames their tools. That combination primed me for a long stretch of unpaid labor in the open-source world, where there's always more thankless work than anyone could finish. Nothing you do is ever quite good enough.
There are real virtues in that world too — being able to inspect how software works, fix it for yourself, and share the fixes. But there's a lesson I wish I'd learned sooner: you're always spending either time or money. When you're young and broke, time is what you have, so the choice makes itself. The second-order effects of all that unpaid work on my career and health have been mixed, but I wouldn't trade the experience away.
The Tooling Gap
Since around 2019, I've been doing serious technical writing, which means I need to produce diagrams. I weighed the options and settled on draw.io (semi-rebranded to diagrams.net, in the way Twitter became X). The desktop app still calls itself draw.io, and the file format is still .drawio.
It's unmistakably an Electron app:
But it behaves consistently across Linux, macOS, Windows, and the browser. And it's genuinely good at diagramming. Modern alternatives like Mermaid or Excalidraw are source-defined and browser-native, but draw.io earned its place by solving a hard problem first: building a graph editor that could render SVG and still work in browsers without SVG support. That heritage goes back to mxGraph in 2005, a commercial project that ran until 2016, when the non-SVG browser advantage expired.
We created mxGraph in 2005 as a commercial project and it ran through to 2016 that way. Our USP was the support for non-SVG browsers, when that advantage expired we moved onto commercial activity around draw.io.
That spirit of taking the problem seriously carried through the product and made it feel better than the alternatives at the time. What else was there? Dia, whose download page still offers binaries for Solaris 9 Sparc and Irix 6.5?
There's a German site, last updated in 2014, offering Windows and macOS builds:
The screenshot has that early-Vista look: the oversized rounded download button with an unconvincing glass effect, mixed serif and sans-serif fonts, a stock photo of someone drawing on a whiteboard, and a "Donate via PayPal" button. Even the last SourceForge release carries that familiar aesthetic:
Two hand-crafted .dmg files, no CI/CD anywhere, just vibes. That era — SourceForge, Pidgin, Adium — had a certain look and feel that GitHub's rise quietly buried. I don't particularly miss it.
The problem with draw.io isn't the diagramming itself. It's everything around it. When you need to collaborate, share, review, or edit with other people in real time, the open-source toolkit starts showing its seams. Versioning, commenting, permissions — the pieces exist, but they feel bolted on rather than designed.
I've tried the alternatives, and not just the free ones. Some are browser-based, some are desktop apps, some are component libraries you embed. They each nail one part of the workflow and miss the rest. One handles real-time collaboration beautifully but chokes on anything beyond simple shapes. Another renders gorgeous diagrams but has no sane way to track what changed. The self-hosted options put the infrastructure burden on me, which is exactly the kind of ongoing maintenance cost that eats time.
The Text-Rendering Trap
draw.io is still good, and I want to be clear about that. The problem I’m about to describe predates jgraph’s 2016 move to “commercial activity around draw.io,” and a revenue source funding continued maintenance is genuinely a good thing as long as it doesn’t degrade the product.
But somewhere along the way, draw.io stopped investing in its own engine and committed to doing whatever the browser does. And the browser, need I say it, is Chrome.
The docs, to jgraph’s credit, are honest about what this means. “Rich text formatting” is described exactly as what it is: using HTML to style nodes.
Text labels in shapes and on connectors can be formatted with HTML, so you can apply a style to part of a text label. For example, you can apply bold, italicise, colour, or a link to just one word.
Once you let HTML into a product that fundamentally cares about precise layout, you’re in trouble. It’s not literally impossible to build your own browser engine, but it is several orders of magnitude harder than the already-hard problem that mxGraph solves. This is why I keep defending Electron as “not that bad, actually” — there’s more to it than most people think.
In another universe, draw.io picked a limited feature set — bold, italic, underline, font size, font family, line spacing, letter spacing, alignment, superscript, subscript — and wrote its own text rendering engine. Then you wouldn’t need Chrome’s DOM, its layout engine, and Skia’s PDF backend just to get a proper export from a diagramming tool.
But they didn’t, and so my asset pipeline is an elaborate contraption: a handwritten Rust app launches headless Chrome, loads draw.io from a hyper HTTP server, asks draw.io to fetch the .drawio file over HTTP, prints to PDF via Skia’s PDF backend, opens that PDF with poppler, exports it through cairo’s SVG backend, and optimizes with svgcleaner (which is unmaintained and will soon stop compiling).
The result is crisp at any resolution and printable without the font installed. The workflow is nice — hitting CmdOrCtrl+S in draw.io reloads my ftl.localhost:1111 browser tab with the new SVG. I even documented the whole thing, as if this were a normal or desirable state of affairs.
The entire thing is painful to maintain. My Rust app spends several chapters of documentation on getting the “glib cinematic universe” to link statically on Windows; I’ve since resigned myself to running it only where nix is available. And I would sleep better if Chromium weren’t part of the pipeline, because I eventually want to deploy this to servers so other people can collaborate with me.
{
inputs = {
# base nix packages
nixpkgs . url = "github:NixOS/nixpkgs/nixos-unstable" ;
# this lets you build cargo projects inside nix
crane = {
url = "github:ipetkov/crane" ;
# all those "follows" things essentially make sure you only depend
# on one version of package x, even if several other packages depend on x
inputs . nixpkgs . follows = "nixpkgs" ;
inputs . rust-overlay . follows = "rust-overlay" ;
};
flake-utils . url = "github:numtide/flake-utils" ;
rust-overlay = {
url = "github:oxalica/rust-overlay" ;
inputs = {
nixpkgs . follows = "nixpkgs" ;
flake-utils . follows = "flake-utils" ;
};
};
};
outputs = { self , nixpkgs , crane , flake-utils , rust-overlay } :
flake-utils . lib . eachDefaultSystem ( system :
let
version = "1.6.0" ;
pkgs = import nixpkgs {
system = system ;
overlays = [ ( import rust-overlay ) ];
};
# I use Rust nightly for a bunch of thisbecause I can't be bothered to
# wait for features to stabilize. This keeps it DRY, I only have to
# specify "which nightly" in one place.
# This is only available because we pulled in rust-overlay, afaict.
rustToolchain = pkgs . pkgsBuildHost . rust-bin . fromRustupToolchainFile
./rust-toolchain.toml ;
craneLib = crane . lib .${ system }. overrideToolchain rustToolchain ;
# You can't clone this, but I can, and that's where my "private" crates
# live. If you break in, it's no big deal: they're not secrets, just
# "don't ask me questions about them" crates.
craneLibWithRegistry = craneLib . appendCrateRegistries [
( craneLib . registryFromGitIndex {
indexUrl = "https://redacted.example.org/bearcove/_cargo-index.git" ;
rev = "7d22392df559a762b5b87597aad2e5a55847bd41" ;
})
];
# Because we run draw.io as part of the asset pipeline, we need to pull
# in _all the draw.io sources_ (the HTML/JS/CSS part that runs in the
# browser) as build inputs.
# This was fun to figure out.
src =
let
drawioFilter = path : type : builtins . match ".*drawio-assets.*" path != null ;
drawioOrCargo = path : type :
( drawioFilter path type ) || ( craneLib . filterCargoSources path type );
in
pkgs . lib . cleanSourceWith {
src = craneLib . path ./. ;
filter = drawioOrCargo ;
};
buildInputs = with pkgs ; [ poppler ];
nativeBuildInputs = with pkgs ; [
# for poppler, cairo etc.
pkg-config
# to invoke mold (that became unnecessary at some point on some platforms, shrug)
clang_14
# to link faster
mold
# needed for cairo iirc
nasm
# you can do your own research re: autoPatchelfHook and
# get amazed+angry about it on your own time.
] ++ lib . optionals pkgs . stdenv . isLinux [ autoPatchelfHook ]
++ lib . optionals pkgs . stdenv . isDarwin
( with pkgs . darwin . apple_sdk . frameworks ; [
# why aren't these pulled in automatically? fuck if I know
CoreFoundation
SystemConfiguration
Security
]);
cargoArtifacts =
craneLibWithRegistry . buildDepsOnly
{
inherit src buildInputs nativeBuildInputs ;
};
bin = craneLibWithRegistry . buildPackage {
inherit src buildInputs nativeBuildInputs ;
};
# on Linux, we pull in chromium, but on macOS we rely on Google Chrome
# being under `/Applications`: GUI apps are annoying to programmatically
# install under macOS in a way CLI stuff simply isn't (see homebrew
# formulas vs casks, gatekeeper, etc.)
chromiumDep = if pkgs . stdenv . hostPlatform . system == "x86_64-linux" then with pkgs ; [ chromium ] else [ ];
wrapped = pkgs . symlinkJoin {
name = "salvage-wrapped" ;
paths = [ bin ];
buildInputs = with pkgs ; [ makeWrapper ] ++ chromiumDep ;
# we need chromium to be in PATH, so, we make a wrapper script.
postBuild = with pkgs ; ''
wrapProgram $out/bin/salvage \
--set PATH ${ nixpkgs . lib . makeBinPath chromiumDep }
'' ;
};
in
{
packages = {
bin = bin ;
wrapped = wrapped ;
default = wrapped ;
};
devShells . default = pkgs . mkShell {
inputsFrom = [ bin ];
nativeBuildInputs = [ ];
packages = [ pkgs . just ];
};
});
}
Figma Changes the Calculus
In 2023, I was contracted for work related to Figma that I can’t discuss, thanks to a paranoically-worded NDA. But it forced me to actually learn the tool, and an unfair comparison it may be — draw.io is a diagramming tool, Figma is an interface design tool — but Figma is just built different.
I first heard of Figma when someone asked how I felt about them compiling Rust and C++ to WebAssembly for a product that, on the surface, seems like it shouldn’t require more than a large pile of JavaScript and DOM tricks. But they cared about more than that. Evan Wallace’s write-ups from his time as co-founder cover ideas like vector networks — which immediately read as superior to the classical vector path model everyone else uses — and the server-side migration from TypeScript to Rust, which yielded 3.8x smaller memory usage, 6x smaller CPU usage, 10x faster file serving, and 16.4x faster worst-case save times.
The Figma team didn’t solve one hard problem. They solved dozens, all while maintaining best-of-class UX. Even Adobe agreed.
The Price of Not Suffering
But Figma costs money: three free files to explore, then $15/seat/month, or $12 if you commit to a year (I’m not comfortable with that). And I was raised in a culture of frugality so intense that couponing qualifies as foreplay.
In 2023 I announced I was building a team to produce more content. I paid two people for a few months to help with research, triage, and the “platform” part — everything here is custom-made and wonky, by design, because having a lot to write about feeds the machine. But I hate per-seat per-month pricing. For a small business it adds up, to the point where one might wonder: should I just learn Kubernetes, self-host everything, and save money in the long run?
That seems like hard work to get worse things. Which is, of course, exactly how I was raised: if it’s not suffering, it’s not work, and if it’s not work, it’s probably of the devil.
Which brings us to Penpot.
Open-source design tools and the price of freedom
Penpot's pitch is unmistakable from the moment the page loads. The headline occupies a solid chunk of viewport real estate: “Bring Design Freedom to your Product Team,” followed by two calls to action: “Sign up, it’s free!” and “Self-host install.” The tagline underneath reads “Penpot is the Open-Source Design & Prototyping Tool for Product Teams.”
By the time you reach that sentence, the words “free” have appeared twice and “open-source” once. Consider yourself warned.
Dismissing criticism with “PRs welcome” is practically tradition in the open-source world. But the real question is whether the software holds up. I was told Penpot is, of the free Figma alternatives, by far the best. After spending hours with it, my conclusion is that if this is the best, the other alternatives aren't worth investigating.
Each issue I encountered could theoretically be fixed in isolation. That wouldn't change the core problem, which is fundamental to how the product is built.
Self-hosting as the intended path
The homepage mentions a desktop app. There is one, but it's unofficial — a community project running on a forgejo instance that looks like it's hosted on a Raspberry Pi behind Cloudflare. To Penpot's credit, the company sponsors the developer, but the official route for avoiding their cloud subscription is self-hosting.
The self-host documentation first suggests paying a third party to run it for you, which is reasonable. But if you're going down this path to escape subscriptions, you'll likely go all the way and deploy it yourself.
The self-host page leans heavily on the cost argument:
For too long, a critical piece of the software development pipeline has been available solely via pricey proprietary SaaS subscriptions or Mac-only desktop apps.
Penpot brings total freedom for teams by encouraging self-hosted deployments so you can take advantage of your security and backup policies, as well as extending and customizing your Penpot server the way you need.
The Docker option involves a docker-compose file, which is fine for local experimentation but absolutely not for production. The intended deployment target is Kubernetes, where you get health checks, rolling updates, and scaling across nodes.
Penpot consists of three services — backend, exporter, and frontend — plus PostgreSQL and Redis. The idiomatic approach is a Helm chart, which you can find on ArtifactHub. Two options exist: one from CodeChem (updated nine months ago) and one from TrueCharts (six months ago). I picked the former, not realizing it pointed at the 1.16.0-beta tag for all three images.
After considerable trial and error, a working k3s manifest emerged, and the deployment runs. The preview for one font file returns a 404, which suggests a misconfiguration in the Helm chart, since the cloud offering handles it correctly.
The export problem
Recreating a simple diagram in Penpot took far longer than expected, though some of that is unfamiliarity. Export initially timed out entirely; upgrading to the non-beta 1.19.3 fixed some of it. SVG export now works, though it takes several seconds.
Inspect the resulting SVG and you'll find text nodes rather than paths. My diagram labels require the Iosevka font to be installed locally to render correctly. That's unacceptable for diagrams meant to display identically everywhere.
Exporting to PDF first and converting with cairo doesn't help — the output substitutes a generic serif font.
The reason becomes clear when you look at the exporter source. It spawns a Chromium instance and prints to PDF. Text layout and rendering are whatever the browser does, which means:
- Export is inherently slow, since it requires a full browser render.
- Output only matches the editor view if you're using Chromium, not Firefox or Safari.
- Custom font handling is fragile, subject to the same issues as any web page.
Penpot does have a custom fonts feature, complete with detection of mismatched vertical metrics and pointers to tools that fix font metadata. Someone clearly cared about this. It's not Figma's approach — where a small native companion app lets you use all your local fonts from the browser — but it's an attempt.
It still doesn't solve the hard problem. Even when custom fonts work, the rendering pipeline runs through the browser, which is never going to match what you see in the editor on other platforms. And browser-based text rendering means exports will always be slow.
What Figma does differently
Recreating the same diagram in Figma was a markedly better experience. Snapping works — and when most of your interactions are moving and resizing elements, that matters enormously. Stroke settings are granular enough to match draw.io's defaults precisely.
Exporting a frame to SVG is instant; everything happens client-side. The SVG contains paths, not text nodes — the label “RAM stick #1” is an extremely long path definition, cut for brevity here. That's exactly what you want for diagrams that must render identically everywhere.
An SVG path for every glyph may make files larger, but it makes them portable. Designers care about precision because it's their livelihood; I care because even mediocre diagrams should look the same on every screen.
Penpot is several major engineering efforts away from solving this. In fact, they've turned the limitation into a different problem frame: their flexbox model matches CSS, which they market as saving time when developers implement designs in HTML and CSS. But that's not a feature — it's just what happens when you don't have a layout engine.
Figma comes from a different era, one where design and implementation were separate disciplines, where you could design for media other than the web, and where someone could drop out of school with a $100,000 fellowship and build design tools full-time.
The verdict
Open-source alternatives frequently end up recreating surface-level features of commercial products while ignoring the parts that make them usable. If Adobe's acquisition of Figma closes, that will be worth mourning too. In the meantime, the $15 monthly subscription remains the most rational choice for getting work done.



