Datasette Apps: Host custom HTML applications inside Datasette

lumpa1 pts0 comments

Datasette Apps: Host custom HTML applications inside Datasette

Simon Willison’s Weblog

Subscribe

Sponsored by: Teleport — Prevent access bottlenecks. Unify identity. Teleport replaces fragmented identity and access tooling with a single identity layer that security teams trust, and engineers want to use.

Datasette Apps: Host custom HTML applications inside Datasette

18th June 2026

Today we launched a new plugin for Datasette, datasette-apps, with this launch announcement post on the Datasette project blog. That post has the what, but I’m going to expand on that a little bit here to provide the why.

The TL;DR

Datasette Apps are self-contained HTML+JavaScript applications that run in a tightly constrained sandbox hosted on your Datasette application. They can use JavaScript to run read-only SQL queries against data in Datasette, and can run write queries too if you configure them with some stored queries.

Here’s a very simple example and a more complex custom timeline example—the latter looks like this:

Apps are allowed to run JavaScript and render HTML and CSS. They are limited in terms of access—the they run in prevents them from accessing cookies or localStorage and they also have an injected CSP header (thanks to this research) which prevents them from making HTTP requests to outside hosts, preventing a malicious or buggy app from exfiltrating private data.

Datasette Apps started out as my attempt at building a Claude Artifacts mechanism for Datasette Agent, but I quickly realised that the sandboxed pattern is interesting for way more than just adding custom apps to the interface surface and promoted it to its own top-level concept within the Datasette ecosystem.

They’re also a fun way to turn my multi-year experiment in vibe-coded HTML tools into a core feature of my main project!

You can try out Datasette Apps by signing in with GitHub to the agent.datasette.io demo instance.

Why build this?

Since the very first release, Datasette has offered a flexible backend for creating custom HTML apps via its JSON API.

One of my earliest Datasette projects was an internal search engine for documentation when I worked at Eventbrite—it worked by importing documents from different systems into SQLite on a cron and then serving them through a Datasette instance with a custom HTML+JavaScript search interface that directly queried the Datasette API.

I had client-side JavaScript constructing SQL queries, which originally was intended as an engineering joke but turned out to be a really productive way of iterating on the app!

That project, combined with my experience building my HTML tools collection and my experiments with Claude Artifacts, has convinced me that adding a Datasette-style backend to a self-contained HTML frontend is an astonishingly powerful combination.

Imagine how much more useful Claude Artifacts could be if they had access to a persistent relational database. That’s what I’m building with Datasette Apps!

Neat ideas in Datasette Apps

Here are a few of the ideas and patterns I’ve figured out building this which I think have staying power.

This is the magic combination that makes Datasette Apps feasible in the first place. I need to run untrusted HTML and JavaScript on a highly sensitive domain—an authenticated Datasette instance can contain all sorts of private data. The sandbox= attribute lets me run that untrusted code in a way that cannot interact with the parent application—it can’t read the DOM, or access cookies, or steal secrets from localStorage. It can however use fetch() and friends to load content (or exfiltrate data) from other domains. But... it turns out if you start an HTML page with a header you can set additional policies that lock down access to other domains. I was worried that malicious JavaScript would be able to update or remove that header but it turns out that doesn’t work—once set, the CSP policy is immutable for the content of that frame.

Locked down APIs with postMessage() and MessageChannel()

Having locked down those iframes to the point that they couldn’t do anything interesting at all, the challenge was to open them back again such that they could run an allow-list of operations, starting with read-only SQL queries against specified databases.

I built the first version of this with postMessage(), which allows a child iframe to send messages to the parent window. I created a simple protocol for requesting that the parent run a SQL query—the parent could then verify it was against an allow-listed database before executing it.

One of the LLM tools, I think it was GPT-5.5, suggested that postMessage() on its own can be exploited if the iframe somehow loads additional code from an untrusted domain. I don’t think that applies to Datasette Apps, but I also believe in defense in depth, so I had GPT-5.5 help me port to a MessageChannel() based transport instead.

MessageChannel() has the advantage that if a page navigates to somewhere else the channel...

datasette apps html custom javascript from

Related Articles