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.
- Go 1.25
- SQLite (WAL)
- html/template
- Vanilla JS
- Canvas API
- jsQR
- Playwright
- Postmark
- Apache
- systemd
- just
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.