Tom MacWright

2025@macwright.com

I was a fan of Deno's security model

This is in response to “I’m not a fan of Deno’s security model”.

I spent most of the last four years working on Val Town. When I joined, we were running JavaScript code in vm2, hoping that it was a sufficient sandbox. It was not. I advocated for Deno and we switched to Deno, in large part because it provided a stronger sandbox.

This decision wasn’t perfect: most of Deno’s core ideas lost in the end.

  • Node compatibility won out. Bun won a lot of users by being Node-compatible. Yes, a lot of Node-like APIs landed in WinterTC, but lots didn’t. Actually the direction is toward a larger Node core, that includes testing, SQLite, and TypeScript support. Stuff that’s long been obviously userspace, like generating ZIP files, is making its way to Node core. So, this is the radical opposite of the Deno philosophy in which the core would be small, mostly standardized JavaScript globals. Deno backtracked and implemented Node compatibility, but it was too little too late.
  • Deno’s HTTP imports, which were a convenient feature for Val Town’s semantics - import code cross-account with zero friction - got rolled back again and again until Deno’s module system was identical to Node’s. The innovation on the module loading front is evolutionary, in the vlt.io sense, not radical like Deno thought it would be.

But: Deno’s security model. In short: it worked. We bet the company on the security of Deno’s runtime flags, and there were absolutely zero cases in which they were the culprit in a security sense. Now we were using it in a different way than the linked blog post describes: from day one, with --no-prompt. Using a program and interactively approving or denying permissions is not what I imagine as a core usecase for the permissions system: the prompts are really there for you to learn what permissions are necessary and then bake them into configuration going forward. We also didn’t enable --allow-run for anything, and yes, it’s confusing. Same with --allow-env, code in Val Town always ran with an artificial environment. The linked post sort of supposes a different use-case - running new code interactively and wanting to loosen the sandbox a bit. If you run the code non-interactively with a strict sandbox, it’s pretty good?

Now: bwrap. cgroups. Linux namespaces. The correct way to implement this system. Yes, I knew about all of that. And the decision of whether to switch from Deno’s process-based perhaps-faux-isolation to Linux’s strong, generic isolation was a constant topic of conversation.

The suggestion is always to just bubblewrap it. Or to just throw some cgroups on it. It’s always easy in abstract. On your local computer, sure, why not! Mostly we were aiming for cgroups, and it turns out that our hosting provider, as well as nearly every PaaS provider, runs processes inside of Docker, and unless you bless the Docker startup with --cgroupns=host to grant it privileges to mess with the host’s cgroups settings, cgroups will not work. That rules out hosts like Render, Railway, etc. The same goes for linux namespaces: doing them on an unprivileged host is hard or impossible. And sure, you can control everything from top to bottom if you run your own servers on metal, but that requires a lot of devops commitment.

Deno, on the other hand, works anywhere. It works the same on macOS and Windows, unlike bubblewrap, so you can reliably test it on developer machines. It’s clean and simple to deploy and scale.

And personally, I think that language VMs implementing controls like this is cool. For example, Starlark calls its execution ‘hermetic’ by default - unable to access the surrounding environment. Lua supports sandboxing too. Probably the ultimate would be an in-language effect implementation in which a bootstrap script could declare which resources an import is able to use - Effect in TypeScript can’t do that, but I believe Koka can.

In this era of untrusted code and exploits everywhere, wouldn’t it be cool if you could import some NPM module for doing date math and say up front that it can’t access the filesystem or environment variables? OS-level capabilities controls are powerful and reliable but coarse. Language-level controls could add an extra level trust & compartmentalization for applications.

Deno’s security model isn’t perfect. But it was there when we needed it, its guarantees held, and it solved a real problem, and for that I appreciate it.

Convex hull

I’m working through some basic computational geometry and today’s is the convex hull, which I implemented on Observable. Check it out. It’s nothing special, the main thing that I learned is the application of cross products to quickly determining whether a line is making a left or right turn. That was pretty neat and I think it’ll come in handy soon.

Simple Statistics 8

I didn’t expect it, but some things last a long time, and a handful of my oldest open source projects just keep going, and I’m still having fun with them.

commit 0b049220e18e9c01cce618e19d71dd7353a09a7f
Author: Tom MacWright <tom@macwright.org> Date: Wed Feb 8 13:18:12 2012 -0500
First commit

I started simple-statistics over fourteen years ago. Crazy. It’s still going. Now has TypeScript types, lots of functionality, good tests, good documentation.

I just tagged the 8th major version of it: a breaking change that makes a few functions throw on empty inputs and stops downleveling code to be compatible with IE11. I’ve been following semver, so “version 8” can be a little arbitrary. I tagged version 6 when I removed bower and component compatibility - remember those things? Ancient technology.

The project has gotten a lot of contributions lately too. Everyone’s definitely using LLMs, but the quality is mostly high enough to merge after a little back & forth. I’m still reading the code and trying to keep it consistent. Depending on how you measure, the module has really taken off in terms of downloads:

0200K400K600K800K1M1.2M1.4M1.6MWeekly Downloads2023-01-312023-05-232023-09-122024-01-022024-04-232024-08-132024-12-032025-03-252025-07-152025-11-042026-02-242026-06-162026-10-062026-10-271.2Msimple-statisticsa fast, modern browser for the npm registry

Probably some LLM scraping in there, maybe some LLM preference, maybe some big upstream consumers that I haven’t identified, but still in terms of vanity numbers, hitting peak weekly downloads of 1.6M is a real accomplishment for me. I’m trying my hardest to keep it stable and reliable. The release flow is well-tuned at this point and I just reworked how the documentation happens.

To another 14 years, I guess!

Uglify bug

I was reading Feross Aboukhadijeh’s course notes for CS 253 Web Security, specifically bcrypt’s slides about real-world security, and an unexpected face popped up (mine). I filed a bug in the uglify project in 2015 and didn’t think much more about it, but turns out, there’s a blog post building on it as an exploit, a whole CVE in the national database and a critical assignment on GitHub.

Fun! I don’t take any credit because I didn’t even come up with a good reduced test case, Titus did more work on that, and Mihai actually made the fix. But it’s cool to come across an old thread that had an unusually big impact.

New Observable

Observable just released a brand new version. There’s a lot to like about it:

  • It splits the editing & viewing modes so that notebooks are less confusing for readers who don’t read code. Back when we tried to make editing a quick hop from reading, it was too easy to accidentally edit, and that was really overwhelming for people.
  • Like Notebook Kit, it supports JavaScript (and TypeScript) properly. Observable JavaScript (OJS) was a dialect of JavaScript with extra keywords, like viewof, and different syntax, like anonymous blocks. The tradeoffs for a custom language are pretty bad with few exceptions, especially when it so closely resembles another language that people assume everything that works in one should work in another. Trust me - I’ve worked on CartoCSS, Observable JavaScript, and the dialect of JavaScript that Val Town initially ran on. Embracing standard TypeScript is even more important now that lots of people are producing code with LLMs, and the training set for TypeScript is much, much bigger than for OJS.
  • It has a proper LLM chat interface that can fetch data, render charts, read images of those charts, and then those charts can be transformed into notebooks. And notebooks now have a built-in LLM assistant. AI integrations are polarizing but the portion of users who are polarized toward AI are heavy users who completely rely on it. Observable’s previous experiments with LLMs felt a little half-hearted in that they required users to “bring their own API token” to pay for the usage. Now there’s built-in Anthropic-backed models, with a very limited free allowance. Adding a way for people to buy credits seems like an extremely pressing need for the new Observable.
  • It looks like they’ve switched to Vercel as a host for both the product and data connectors. And I think this shipped before the big rewrite, but the website shell is all Next.js based now.
  • The new list of supported languages is: TypeScript, Markdown, HTML, SQL, TeX, DOT, JavaScript, and Observable JavaScript.
  • The TypeScript support in the editor is just fine. I have learned firsthand how incredibly hard it is to create a web-based TypeScript-aware editor with completions, so it isn’t surprising that the completions are incomplete and there are no hover hints. Probably you’d want to run a TypeScript 7 LSP server in a container, similar to how we do with Val Town. But on the other hand, for the trends of 2026, it’s probably more important to take the LLM-assisted code generation angle more seriously.

Updating my table from my last Observable update:

ProductLaunchedLocal/hostedLanguagesOSSLayoutCollaboration
Notebooks~2018Hostedojs, md, html, sqlno*notebooklive
Framework2024Hybridjs, md, sql, many othersyesmixednone/github
Canvas2025Hostedjs, sqlnocanvaslive
Notebooks 2.02025Local*ojs, js, md, html, tex, dotyes*mixednone/github
New Observable2026Hostedts, js, md, HTML, SQL, TeX, DOT, ojsnonotebooklive

Backstory/full disclosure: I was the first hire at Observable and hold equity in the company.

Lawnmower story

My dad once bought a lawnmower off of Craigslist, because my family needed a new lawnmower. It was listed as being broken, but after a few minutes of wrenching, it worked perfectly. He was noticeably disappointed.

I’ve been riding unnecessarily long distances on my bike. It’s a dirty, sweaty, occasionally painful sport. A fellow programmer asked why I don’t get an electric bicycle so I could ride faster and with less effort.

The new geojson.io

Good news from the folks at Mapbox - they’ve revamped geojson.io using some of the bones of Placemark, which I open-sourced in 2024. It’s pretty poetic for things to come full circle like that - Placemark was in many ways an attempt to fix some of the things that I thought were deficient about geojson.io, and now geojson.io has those things fixed because of the Placemark codebase.

Chris and Andrew were super nice about it and I think it’s a fantastic outcome. See also epanet-js, which is also based on the Placemark “stack”! It’s not perfect, but it’s really nice to see the open source code help people get to square 1 with new geospatial applications.

Subscription minimizing

I’m feeling more repelled from software subscriptions recently, primarily for products that have their own proprietary data storage or formats. Partly it’s the lock-in effect, and also the platform decay that is accelerating as all companies (besides major AI labs) get nervous about their margins and sustainability.

Anyway, so far:

  • From Fantastical to BusyCal. They’re both proprietary mac-assed Mac apps. I can’t find an open-source alternative with enough polish & maintainership. BusyCal is a version-based permanent license, $50, versus Fantastical which is $57/year. The version-based restriction is a gamble: maybe there’ll be some big feature they ship or macOS change that necessitates upgrading, maybe there won’t be. I’ve been surprised at how little I care that I’m using an old version of DevonThink with a perpetual license.
  • From Feedbin to NetNewsWire. Also a mac-assed Mac app, and lovingly maintained by Brent Simmons, who’s now retired and works on it semi-full-time. It’s solid. Having switched, I’m not sure why I needed my RSS reader to be a web application in the first place. NetNewsWire is an open source application, but you wouldn’t know from using it. It looks good!
  • From Instapaper to Readeck. This is the one I have the most feelings about, being a years-long Instapaper fan. Both it and Feedbin aren’t evil corporations but relatively small, friendly indie companies. But without full-text export, I’m building a proprietary dataset in Instapaper which I don’t want to. And I’m pretty disappointed that Instapaper’s Kobo integration, the reason why I bought a Kobo Calibre Color, never got the basic ability to highlight articles. They rolled out Boox Palma integration a few days ago and it looks like that supports highlighting. I like the idea of contributing to Readeck and the Readeck Kobo plugin supports highlighting and works pretty well, so I’m excited about the possibility there. And readeck of course supports full-text export. So far this is in no way a cost-savings: the Hetzner server I use to host Readeck will cost around $22 a month. I’ll either downgrade it, host something else on it to make it worthwhile, or live with the tiny cost increase. Maybe this is a good opportunity to host a Tranquil PDS to make my Bluesky identity independent.

TODO

Still, my most expensive subscription is the hardest to cut: Capture One has been my go-to for organizing and light retouching of photos and drawings. It’s great, but expensive and likely to get more so as it gets revenue-maximized and sold from one private equity firm to another. I’ve tried digiKam but it’s ugly and the learning curve is steep. Aphera looks amazing but is much more focused on raw editing than organization and lacks any kind of retouching tools. I could return to Lightroom but I dread Adobe products. DaVinci Resolve Photo could be the answer, but there’s no support for Olympus raw yet. I could even use Apple Photos, but I don’t think Olympus raw is supported there either. What I’m really looking for is more of a smooth, capable ‘digital asset manager’ that’s good for managing images. The hunt continues. Maybe buying a perpetual license to Capture One is the answer, but the break-even with my pricing would be at two years.

Encodings

If you’re encoding some binary data as text, you have a lot of options. My cheatsheet:

encodingtransport-safelengthstandardcase-sensitive
hexsafelongyesno
base32safemediummehno
base58safeshortnoyes
base64mehshortyesyes
base64urlsafeshortyesyes
  • transport-safe mostly meaning whether it’ll survive being a url parameter. base64 includes + and - which necessitates base64url.
  • everything technically has a standard but the question is if they’re standardized, and for example node.js only supports Buffer encoding to base64, base64url, and hex.
  • i enjoy @scure/base for using JavaScript and encoding in other base systems.

My current defaults:

  • base58check for API tokens. Short, quick to validate if it’s a valid token or not. Unlike base64, fewer ambiguous characters.
  • base64url for encrypted cookies.
  • base64 for putting longer text into the environment, like JWKS keys or certificates. Somewhat safer than sticking the raw text in there.
  • base32 for user-facing tokens, like invite codes. Good combination of efficiency and safety. Very safe to copy & paste.
  • hex for encryption keys in the environment. Doesn’t really matter that they’re a little long, super easy to generate and copy and paste with openssl rand -hex.

I feel a pull to consolidate these further and ask whether I’m being too fancy using different encodings. In particular, whether all encryption keys should be base64 encoded, and whether we really need base32 for user-facing content. Though base32 subjectively looks nice and is short, it’s pretty nonstandard: no support in Node, and the main place where I see base32 in use, in the AT Protocol, they use a different character set than usual.

Effect notes: v4, Effect.promise, and TaggedEnum

An update since the last Effect devlog:

I finally vanquished the last bits of neverthrow from the codebase and replaced them with Effect abstractions. neverthrow was great for what we used it for - I’d still use it on a simpler project, but Effect is a better fit for Val Town.

The LLMs are getting incrementally better at writing Effect code: they now do a decent job of annotating functions with explicit type parameters, but they still leave some refactoring debt where they call Effect.runPromise repeatedly instead of combining calls with Effect.gen.

Effect v4… I’m not using it yet. The beta has been the focus of the Effect team since February. Old-fashioned Effect v3 is only getting backported fixes. A lot of the community is using v4 and living on the edge. I’m not using v4 for the simple reasons: I don’t want to use beta modules with lots of churn in a production application, and v4 is not documented.

The documentation problem is still pretty bad. The JSDoc issue got closed. The SQL-is-undocumented issue got closed in anticipation of the v4 release and the upcoming focus on documentation. This team is producing incredible code and I hope that they can produce incredible docs to match, but the fear that documentation simply isn’t highly valued or prioritized persists because I’ve seen so little improvement.

I’ve written some more ast-grep rules for Effect. We have a big codebase and there are lots of ways to screw up. Some of the ways that I’ve recently automatically prevented are:

Throwing in Effect.gen! This is going to be refactoring debt one way or another. If you run into an issue, you yield an error, not throw:

Here’s my rule:

id: dont-throw-in-effect
language: Tsx
rule:
  pattern: Effect.gen($$$A)
  has:
    pattern: throw $A
    stopBy: end

Using Effect.promise. Early on, I used this method a bunch because I was being lazy. The LLMs probably used it too. I’ve learned that it’s a footgun: if the promise it wraps ever rejects, that produces a defect, not a failure, and that goes through a totally different error handling path. We should always use Effect.tryPromise.

Here’s the rule:

id: no-effect-promise
language: Tsx
rule:
  pattern: Effect.promise($A)
fix: 'Effect.tryPromise({ try: $A, catch(error) { return new UnexpectedError({message: "Unexpectedly failed", cause: error }) } })'

And then update the “Unexpectedly failed” message with something more descriptive.

Data.TaggedEnum is the new toy. I use a lot of discriminated unions. If there’s a x in y type check, that’s a code smell to me and we should use a type or kind field to differentiate the different object shapes instead. TaggedEnum cuts some of the boilerplate for that and adds some in another area. I like it but I’m not 100% sure the abstraction pays for itself yet.


At this stage, Effect is pretty productive, though a lot of our initial issues remain. The application is far from being a ‘pure’ Effect app, but most important database-touching, authentication-enforcing, or business logic-producting methods are using Effect’s abstractions. I have no regrets about the switch.

Everything I've written about everything in the archive…