Work dubloons
platform

Dubloons

Struck metal collectibles whose QR code opens a media experience we host — not a link that will one day rot.

Dubloons are collectible coins you can hold, each carrying a QR code that opens whatever that coin is for — an album, a film, a gallery, a game, a joke. The media is hosted on the platform rather than forwarded to somebody else's URL, so the coin keeps working after the streaming service, the label or the artist's website has gone. The metal never changes; what sits behind it can, forever. Built on a Go and SQLite stack with an experience layer, a moderation queue that fails closed, and a browser-rendered digital twin of every coin whose QR is decoded in CI to prove it still scans.

Last updateJul 31, 2026 LicenseNOASSERTION PrimaryGo
  • Go 1.25
  • SQLite (WAL)
  • html/template
  • Vanilla JS
  • Canvas API
  • jsQR
  • Playwright
  • Postmark
  • Apache
  • systemd
  • just
Dubloons — Struck metal collectibles whose QR code opens a media experience we host — not a link that will one day rot.
Dubloons media

Dubloons are struck metal coins with a QR code on one face and a hosted media experience behind it. Scan one and it opens what that coin is for — a record, a short film, a photo set, a playlist, a browser game. The distinction that matters is that the experience lives on the platform. A coin that forwards to a third-party link is only as permanent as that link; a coin that opens something we host stays honest as long as the domain does.

The encoding decision the whole product rests on

The QR carries DUBLOON.UK/A7K in uppercase, and that is not a style choice. QR's alphanumeric mode covers digits, A–Z and a handful of symbols — no lowercase — and it is far denser than byte mode. Fourteen characters uppercase fit a version-1, 21×21 symbol at error correction Q; the identical string in lowercase forces byte mode, a version-2 25×25 symbol, and drops to correction level M. On a 28 mm coin panel that is 0.97 mm per module against 0.85 mm, with ten points more redundancy on top.

That margin is the difference between a coin that still scans after five years in a pocket and one that does not. Two independent mechanisms make an uppercase scan resolve to a lowercase slug — path normalisation before lookup, and a COLLATE NOCASE slug column — and a named test asserts both, because if either regresses every coin already in circulation stops working.

The coin exists on screen before it exists in metal

  • A rendered digital twin. Every Dubloon draws as a spinning struck coin on a canvas — eight metal finishes, the name stamped into the reverse, the spin dwelling face-on, click to freeze it. The QR on the rendered coin is real and scannable off the screen.
  • A decoder in CI. One command drives a headless browser over every coin in the fleet, decodes each rendered QR with jsQR, and asserts it equals the payload the server would serve. It is the only check that catches a coin that renders beautifully and cannot be read — the failure mode you otherwise discover after the dies are cut.
  • Mockup QRs that must not scan. Design previews carry deliberately invalid codes, and that is tested too, so a mockup can never escape into production artwork.

Hosting media without becoming a warez locker

A platform that hosts other people's media is one upload away from being an infringement problem, and the hostname is cast into every coin — a rights complaint that takes the domain down would break coins already sold. So the media bucket is gated: the platform carries only content the operator owns or holds a release for, an admin queue moderates every asset, and the moderation layer fails closed — an asset that has not been cleared does not serve. Crews and roles decide who can push what.

How it's built

One Go binary and one SQLite file, rendering through html/template, with a vanilla JavaScript front end and the coin renderer on a raw canvas — no framework and no bundler. The experience layer adds experiences, assets, asset_variants and coin_runs to the schema, all through additive-only migrations, because a coin struck today has to resolve against every schema that comes after it. Deployment is a systemd unit behind Apache with Let's Encrypt on a single host.

An admin console covers the fleet: minting a coin, filling it with media, working the moderation queue, and choosing where a run gets struck. Coin art is generated through an image-model router with a hard budget cap wired into the studio, so an art session cannot silently run up a bill.

Straight from the source

The project's own README.

Rendered in place — every link, image, and code block carried over from the repo. The page below is what a contributor would see opening the project for the first time.

Dubloons

Struck metal coins carrying a QR code we host. Scan one and it opens whatever that coin is for — an album, a film, a gallery, a game, a plain link. The metal never changes. What sits behind it can, forever.

Live: https://dubloon.uk · service dubloons, port 8141

Private. Not for distribution.

Getting oriented

Read docs/quick-start.md first — stack, commands, hard rules, layout. Status lives in docs/PROGRESS.md; the founding directive is in docs/project/directive.md.

Stack

Go 1.26 stdlib + mattn/go-sqlite3 · SQLite (WAL) · html/template · vanilla CSS/JS · Postmark · systemd + Apache reverse proxy. No frameworks, no node_modules.

Commands

just build      # compile to bin/dubloons
just dev        # run locally with race detector
just test       # go tests, scrub gate included
just ship       # test + build + restart + health check
just logs       # journalctl -u dubloons -f
just scrub      # static scan for ancestor / off-limits words

The one thing to know before touching the slug engine

Every coin's QR carries DUBLOON.UK/<SLUG> in uppercase, so it encodes in QR alphanumeric mode — a full symbol version smaller and one error-correction level higher than the same string in lowercase. On engraved metal that margin is whether the coin still scans in five years. Slugs are 2–5 characters.

An uppercase scan must resolve to a lowercase slug. Two things guarantee it — redirects.Normalize and COLLATE NOCASE on the column — and TestUppercaseCoinPayloadResolves asserts both. Every coin already in someone's pocket depends on that test staying honest.

Build something like this

Want a tool like this for your shop?

We've shipped this kind of thing before. Twenty-minute intro call, no slides.