BLACKFYRE
MG—01
LANG
←
ALL POSTS
05 · Writing / 2026
08 OCT 2026 · 15 MIN READ

Why the Web Gallery of Art Runs on PocketBase, HTMX and Vanilla JavaScript

What the rebuilt Web Gallery of Art runs on (Go, PocketBase, SQLite, templ and htmx), and how a read-heavy catalogue ended up with such a small stack.

CONTENTS
13 +

When you modernise a thirty-year-old website, you’re allowed to replace almost everything. That’s also the quickest way to turn a working cultural resource into an expensive technology demo, so when I started rebuilding the Web Gallery of Art I wasn’t shopping for a fashionable stack. I needed something cheap to run, that a future maintainer could understand, that search engines and screen readers would be happy with, and that wouldn’t drag in more infrastructure or dependencies than necessary.

What it ended up running on is Go, PocketBase with SQLite, templ for HTML, htmx, a modest amount of TypeScript, plain browser APIs, and S3-compatible storage for the images. No React, no Next.js, no separate frontend talking to a separate backend, and no Kubernetes cluster quietly contemplating its own importance. I didn’t set out to avoid any of those. They just didn’t fit what the gallery does.

What the site actually does

WGA is a catalogue, and almost all of its traffic is reading. There are roughly 60,000 artwork images, each with three thumbnail sizes; the archive was about 12 GB when I started and has grown a little as I’ve added optimised variants. The site gets around 723,000 requests a day, which is a fair amount, but the requests are simple: people browse, search, open artist and artwork pages, and move between collections. Nobody is doing thousands of concurrent writes.

So there’s no need for high write throughput, real-time collaboration, state spread across regions, heavy client-side state, or horizontal scaling from day one. The study board and the itinerary creator are properly interactive, but they’re a small corner of the site. Once I’d accepted that, a lot of the usual modern stack looked like paperwork I didn’t have to fill in.

How the pieces fit

The browser talks to one application. PocketBase is the foundation and the database layer, on SQLite; Go code built on top of it serves the public site, runs the background jobs and holds everything specific to WGA; the image files sit in object storage. There’s nothing else the application needs in order to run. A request goes from the browser to Go and PocketBase, and from there to SQLite or the images, which means a new maintainer can follow it without drawing seven coloured boxes and a message queue first.

Why there’s no SPA

Most of what visitors do is follow links. The server returns a page and the browser shows it, and browsers have had a few decades to get good at exactly that. Turning WGA into a single-page application would move page building and routing into the browser without fixing anything for most of the site. The study board and dual-pane browsing do behave more like applications, but plain JavaScript handles them, so the rest of the site can stay server-rendered.

PocketBase

I’ll admit I like PocketBase. Its minimalism appeals to me, and I’d been waiting for a serious project to use it on. That isn’t a reason to put it into production, so it’s just as well there were practical ones.

It gives a self-contained deployment most of what I’d otherwise have assembled myself: database access and SQLite management, migrations, an admin interface, APIs, file handling, authentication if WGA ever needs it, and the web server and runtime. The admin interface mattered most. Go still has nothing like the admin panels Django, Laravel or Rails developers take for granted, and I’d rather not spend part of my life writing yet another CRUD interface.

WGA doesn’t treat PocketBase as a ready-made backend behind something else. It’s the application framework. Nearly all of the public pages and interactions are PocketBase extensions written in Go, and so are the scheduled jobs: generating the sitemap, sending postcards, cleaning up the database and other maintenance. PocketBase provides the base, and the application owns its own routes and behaviour.

SQLite, and what it costs

Developers get oddly emotional about SQLite. For WGA the reasoning is dull: reads dominate, and nothing needs distributed writes, sharding, or several nodes writing to the same relational state. PocketBase makes strong performance claims for its SQLite setup, and WGA is a good real-world, read-heavy test of them. That’s also why OpenTelemetry went in early. If the database turns out to be the problem, I want to see it in the traces, and I want to have ruled out the application’s own inefficiencies before blaming SQLite. If it does come to that, there are PostgreSQL-compatible PocketBase forks to look at.

The trade-offs are real. There’s no sharding, no conventional redundancy, and scaling out isn’t an easy fix for slow code. I don’t mind that last one. If the application wastes work, it should stop wasting it, not spread the waste across more machines. When a visitor gives up on a request, for example, the work for it should stop, rather than carry on because another instance could, in theory, soak up the cost. Whether all this holds up as traffic grows is something the measurements will show.

Server-rendered HTML with templ

Pages are rendered with templ, which gives Go typed HTML templates (whoever named a templating system templ was presumably amused). Layouts, pages, fragments and components are reused throughout, and full pages and htmx fragments come from the same components. That covers most of what people like about frontend component frameworks, such as reuse, composition and consistent structure, except the components run on the server. There’s no hydration step and no virtual DOM rebuilding a page the server already rendered. The browser gets a document and does its ancient, apparently unfashionable job of displaying it.

htmx and plain JavaScript

Server rendering doesn’t mean a full reload for every click. htmx swaps parts of the page where that’s noticeably nicer: pagination, search, filters, forms, detail views and navigation. That’s enough for the site to feel current without the browser becoming a second runtime that re-implements the server.

It also means WGA mostly works without JavaScript. Switch it off and nearly everything still works. The exceptions are the genuinely client-side features, the study board and image magnification. JavaScript adds theme switching, bionic reading and smoother transitions on top, but the catalogue is still a website underneath. That’s progressive enhancement in a (sadly) unusually literal sense.

With htmx doing the talking to the server, very little client-side logic is left, which makes a frontend framework hard to justify. Frameworks come with dependencies, conventions, build tooling and upgrades, and you start paying for those straight away, whether or not you use what they offer. WGA uses TypeScript for type safety and ships ordinary browser JavaScript. TypeScript is about 9.9% of the repository, test suite included. The browser receives roughly 655 KB of JavaScript in total, and more than half of that is dependencies like htmx, Viewer.js and Trix. For a site with this many features I can live with that, and it’s less than a lot of frameworks weigh before you’ve written a line of your own.

Will the plain JavaScript turn into a framework of its own eventually? Probably, at least a little. Write reusable code for long enough and someone will accuse you of inventing a framework. I try to keep the helpers small, obvious and documented. Web Components aren’t used much yet, but they’re available if something needs a firmer boundary. Dual-pane browsing is a likely candidate, since it’s desktop-only, outside the main SEO path, and interactive enough to deserve its own custom element. The browser already provides that; there’s no need to install anything for it.

HTML that means something

Semantic HTML was as much a reason for server rendering as performance was. The old WGA comes from a time when markup mostly described what things looked like. The rebuild uses elements for what they are (navigation, articles, figures, headings, forms) and adds Schema.org metadata for machine-readable context. Screen readers and search engines benefit, and so do the automated systems that increasingly read the web. Whatever those systems become, clearly labelled content beats making them guess what a pile of anonymous divs was supposed to mean.

Because the server returns HTML instead of JSON for a client app to turn back into HTML, a lot of usual frontend work simply isn’t there. Bots can read the pages, screen readers can navigate them, people can bookmark them and links behave like links. Deep links work because every page is a real URL, not because a client-side router recreates them.

The data was the hard part

Choosing the stack was easy compared with migrating the content. The dataset grew over three decades with very few enforced conventions, and normalising it took several attempts, a lot of manual work and an impressive collection of exceptions. Before LLM-assisted development was practical, a lot of it looked like it would need costly manual processing. LLMs did change that, though not by touching the data: they don’t process the production dataset, they helped me write the deterministic parser that does. The migration is still plain, reproducible code, and the same source material always gives the same output, which is what you want with cultural and historical data.

Some things shouldn’t be normalised away. Modern software assumes people have a first and a last name; history never got that schema. Albrecht Dürer fits it reasonably well, Leonardo da Vinci doesn’t, because “da Vinci” isn’t a surname, it says he was from Vinci. Squeezing historical names into modern structures makes the data tidier and less correct. So WGA normalises where it can and keeps room for the irregular cases, and uncertain dates keep their ca. or ~ instead of pretending the database removed the uncertainty.

Very little is machine-derived so far. Colour palettes are extracted from the images and kept out of the normal editorial admin, and anything expensive to compute is done ahead of time, never during a request. I’ve thought about tonal or semantic analysis with language or vision models, but I’m wary. WGA has paintings, sculpture, architecture and more, and a model trained around ordinary images won’t necessarily say anything useful about all of them. Having the option isn’t a reason to use it.

Images, deployment and money

The images aren’t in SQLite. That shouldn’t need saying, but put enough developers near a keyboard and anything becomes a debate. Sixty thousand originals plus thumbnails would make the database enormous, and bundling them with the application would make every deploy heavy and tie storage to it. PocketBase keeps them in Railway’s S3-compatible object storage: the application holds the catalogue and which file belongs to which record, and the storage holds the files.

Deploying used to be a batch job a few times a year, partly because pushing large amounts of material over SSH took a long time, and copying gigabytes is still copying gigabytes even with rclone. The new application deploys on Railway whenever something lands on main. That’ll get more controlled before the first production release, but deployments are already reproducible, which is the part that matters. The original site still runs on Lighttpd, on servers run by the KFKI Research Institute for Particle and Nuclear Physics. Railway isn’t meant to be the host for the next few decades; it’s the one that fits the project now with a reasonable amount of effort, and the application shouldn’t depend on it more than it has to.

Predicting the running cost is difficult, because the architecture is so different from the old one and so much of the current traffic is bots. My rough target is about €10 per 5,000 daily active users under realistic conditions; at today’s traffic, bots included, I expect €50–100 a month. Cost matters because WGA isn’t a commercial product and I’d rather not put ads in front of visitors. But a small stack isn’t only about the bill. Every extra service is another thing that can fail, another upgrade, another account and credential, more documentation and more to understand, and in my experience the expensive parts and the fragile parts tend to be the same ones.

PocketBase backs up daily to S3-compatible storage, and schema changes go through its migrations. I only move the schema forward and don’t maintain reversible migration chains; if data ever needs restoring, that’s what the backups are for.

Sentry sponsors the project through its open-source programme, and it has already caught bugs during development that would have been hard to reproduce and might have gone unnoticed for a long time. OpenTelemetry covers the application side. I’m not trying to build an observability department for an art gallery. I just want to be able to see where time goes, which requests are expensive, whether SQLite is actually a bottleneck, and whether a failure came from my code, the storage, rendering or something else. Since I’ve decided not to scale out by default, I need to know why something is slow.

The alternatives I didn’t pick

React or Next.js would have meant two applications to serve one website: a backend that fetches and prepares data, and a frontend that asks for it across a boundary and builds the pages again, each with its own build system, dependencies and upgrades. Even in one container, that boundary would still be there. It’s a good design for plenty of systems, just not this one.

SvelteKit was the tempting one, because I actually like Svelte. But WGA doesn’t get simpler because the framework is one I enjoy; it just moves the complexity around. The real question was whether the project needed a frontend framework at all, and so far it hasn’t.

PostgreSQL would replace something that works with something that has a better reputation for scaling, which isn’t a good enough reason. If profiling ever points at SQLite, the PostgreSQL-compatible forks are there. A traditional CMS would duplicate most of what PocketBase already does, with another big system to maintain.

Static HTML was genuinely attractive. The original WGA proved for thirty years how long static content can last, and it’s very cheap to serve. The problem is the study boards, itineraries and dual-pane browsing: doing those properly on top of generated pages would take a lot of client-side code and rewriting, until you had a static site with an application bolted awkwardly on top. Server-rendered HTML keeps most of the advantages of static pages and still has a runtime where it’s useful, and with some caching most of the cost goes away, without rebuilding the whole site to fix a comma.

Microservices aren’t justified by anything in WGA’s current size. PocketBase already separates the admin side from the public side, and the code is organised by area, so pieces could be split out later if they ever need to be. I’d be delighted if the gallery outgrew PocketBase one day, but I doubt it will.

Keeping it maintainable

For me, “boring technology” means proven, stable and capable, not old for the sake of it. htmx is young next to the web itself, and PocketBase is still changing. What I care about is whether a tool reduces what you need to understand or just moves the complexity somewhere more fashionable. Dependencies cost more than hosting does: upgrades, breaking changes, tooling, specialist knowledge, abandoned packages, build systems, friction for contributors, and the bus factor. WGA is mostly a one-person project on the technical side, so that matters even more. Contributions are always welcome, and Go is already enough of a hurdle for some people; there’s no need to add five more ecosystems before anyone can change a button.

I’ve always written code on the assumption that the next developer is a crazy person with a hatchet who knows my address. It does wonders for readability: ordinary solutions over clever ones, established conventions, visible boundaries, documentation and predictable behaviour.

Nobody knows whether anyone will want to rebuild WGA in 2046. The way people find and read information is changing enough right now that betting on another thirty years of ordinary web browsing would be brave. I’m not trying to freeze today’s web until 2056, just to keep WGA useful for as long as possible and make sure its software isn’t what eventually kills it. If a Go compiler still exists in twenty years and the dependencies can still be found, it should still build. And I plan to keep maintaining it, because no architecture keeps itself alive.

How it’s going

The choice I’m most sure about is htmx plus plain JavaScript. Frontend bloat had become the part of modern web development I found hardest to defend, and dropping most of it has been a relief, without the site losing its interactive features. Pages are pages again, rather than an application shell waiting to become one.

The main friction came from PocketBase itself. A big architectural change around version 0.20 meant extra work. That wasn’t a surprise, since PocketBase has always been clear that it’s pre-1.0 and may break things, and if you choose younger infrastructure you don’t get to be indignant when the warning turns out to be true. So far it’s been worth it.

Would I pick the same stack again? For WGA, yes; there’s nothing I’d swap out just to swap it. I wouldn’t recommend it everywhere, though. For heavily administrative applications, big write workloads, multi-region deployments, anything that needs to scale out, or real-time collaborative software, I’d think again, because those are different problems.

That’s really the whole argument. WGA runs on PocketBase, htmx and plain JavaScript because they suit this particular site: it’s read-heavy, its content needs stable URLs, its pages have to make sense to browsers, search engines and screen readers, it has to be cheap to run, a small number of people have to be able to understand it, and most of what visitors do doesn’t need a framework in the browser. Written down, that sounds obvious. It’s still common to pick the technology first and find the reasons afterwards. With WGA I tried to start from the problem, leave out what it didn’t need, and make the rest work properly.

TAGS
Web Gallery of Art
PocketBase
htmx
Go
06 · CONTACT

Have a system that needs building?

GET IN TOUCH → PROJECTS →
PRODUCT DATA SHEET
MG—01
BLACKFYRE
S/N MG-1985-1027
Miklós Galicz — Golang Advocate · Solution Architect
MODEL
MG—01 "Miklós Galicz"
SERIES
1985
ORIGIN
Nagykovácsi, Hungary
FUNCTION
Senior Full Stack Engineer · Solution Architect
CORE LANGUAGES
Go · PHP · JavaScript
SPOKEN
Hungarian · English · German
SERVICE LIFE
~20 years in software, ongoing
POWER SUPPLY
Coffee, 2–4 cups / day
DIMENSIONS
1 × human, standard size
OPERATING TEMP.
Calm under production incidents
CONNECTIVITY
[email protected] · github.com/blackfyre · linkedin.com/in/galiczmiklos
Less, but better. Specifications subject to continuous improvement.
● ● ●
MG—01 · SERIES 1985
№ MG-1985-1027
CERTIFICATE OF OPERATION
Certified Operator
Has located every documented feature of the MG—01 without reading the manual. Probably.
TIME
—
FEATURES
—
DATE
—
SIGNED
Miklós Galicz