27 AUG 2026PRODUCTO'ZBEKCHA

Tadam: a wedding album with no app and no database

At an Uzbek wedding there are three hundred guests and three hundred phones. By the end of the night those phones hold the only photographs of the evening that show anything real — the tables, the dancing, the grandmother laughing at something off-camera. The couple will see about twenty of them, forwarded into a group chat and compressed into mush. Tadam is my attempt to fix that with a QR code on the table.

It went live at tadam.uz on 27 August 2026. This is how it works and everything that was harder than it looked.

The flow

The couple creates an album — a title and a date, nothing else, no account. They get back three links: a guest link, a management link, and a printable QR sheet. The QR sheet goes on the tables.

A guest scans it, the camera roll opens, they pick photos, they upload. No app, no sign-up, no password. Then the couple opens the management page and downloads everything as a single ZIP.

Tadam homepage showing the four-step flow
The homepage. The entire flow is explained before you scroll — because most visitors arrive from a Telegram link and will not read.

The design constraint that drove everything above the fold: someone sees this link dropped in a wedding Telegram group. They have about four seconds of curiosity. If they have to think about what the site is, they leave. So the four steps are visible immediately, illustrated, with no marketing preamble in front of them.

No app, and no database either

The no-app decision is obvious once you picture it: nobody at a wedding, holding a plate, is going to install an app. It has to be a web page that works on the first tap.

The no-database decision was less obvious and I'm still pleased with it. The whole thing runs as a Cloudflare Worker with R2 as the only datastore. There is no SQL, no KV namespace, no Durable Object. Album metadata lives in R2 object metadata; the photo list is an R2 prefix listing. A wedding album is written once, read a few times, and then never touched again — that's a workload that does not need a database, and every database I didn't add is a thing that can't go down at 11pm on a Saturday.

The name

It was called Chaqnoq for the first three days. Then I said it out loud to someone and watched them fail to spell it back to me. A wedding product is spoken about, not typed — one person tells another at a table. "Chaqnoq" has a consonant cluster that survives neither speech nor a bad phone speaker.

Tadam is the sound a magician makes when the trick lands. It's two syllables, it's spelled how it sounds in Uzbek, Russian and English alike, and it means the same thing in all three.

Renaming a live codebase is where you find out how many places a brand name hides. I wrote a rename script that was context-aware — it knew the difference between the brand in visible text, the brand in a CSS class name, and the string in an API path that must not change — with a mandatory dry run that printed every match before touching anything. A blind find-and-replace would have quietly broken the URLs of every album already created.

The QR code, measured rather than assumed

The QR sheet is the physical product. If it doesn't scan from a seated guest's arm's length, in dim wedding lighting, on a sheet that has had somebody's plov on it, nothing else matters.

Printable QR sheet for a Tadam album
The printable QR sheet: 64 mm code on A5, error-correction level Q, with the typed URL underneath as a fallback.

So I measured it instead of hoping. I rendered the QR at the pixel sizes a phone camera actually sees it at from various distances and ran a decoder against each one. It reads reliably from about 15 cm out to 85 cm, which covers everyone who can reach the sheet. It's generated at error-correction level Q, which by specification reconstructs the payload with roughly a quarter of the code damaged — that's the stain tolerance.

Then the link underneath it. Some guests won't scan; they'll squint and type. The URL was tadam.uz/e/?i=4se48ya9r — 23 characters including a question mark and an equals sign, both of which are a nuisance on a phone keyboard. It's now tadam.uz/4se48ya9r, 18 characters, all lowercase letters and digits. A single-segment path 301-redirects to the real album URL, with a reserved-name guard so that a future /about or /qr can never be mistaken for an album ID.

I was asked whether the ID itself could go from nine characters to seven, and I said no. The privacy model of this product is that the link is the password — anyone holding it sees the photographs. Dropping two characters shrinks the keyspace by a factor of about a thousand. Five saved keystrokes is not worth a thousand-fold weaker guess resistance on other people's wedding photos.

The parts that were actually hard

Trusting nothing about an upload. A file's extension and its Content-Type header are both just claims made by the client. Every upload is sniffed by magic bytes — JPEG, PNG, WebP, GIF and HEIC, the last one requiring a walk into the ISO-BMFF ftyp box because iPhones are the primary camera here. Anything that isn't a real image is refused, and the size is rejected from the Content-Length before the body is read rather than after.

Serving user content safely. Photos are served under default-src 'none'; sandbox. Even if someone found a way to store something executable, the browser has no permission to run it, reach the network, or touch the parent page. The site itself carries HSTS, nosniff, a referrer policy and a permissions policy.

Random IDs that are actually uniform. The obvious way to pick a random character is random_byte % alphabet_length. That is biased whenever the alphabet doesn't divide 256 evenly, and early characters come up more often than late ones. I measured it over two million samples: 12.65% bias. Replaced with rejection sampling, which throws away the out-of-range draws and re-rolls.

Delete, without an account. Every photo gets a delete key derived by SHA-256 from the album and the photo, so a guest can remove a photo they just uploaded and regretted, and the couple can remove anything. The key comparison is constant-time — a timing-leaky comparison would let someone guess a key one byte at a time.

A ZIP with a progress bar. Streaming a ZIP out of a Worker gives you no Content-Length, so the browser shows an indeterminate spinner and the couple has no idea whether a 400-photo download is at 10% or 90%. FixedLengthStream is the only mechanism that gets a real length onto a streamed Worker response. Computing that length in advance meant working out the exact byte size of the ZIP before generating it.

Tadam guest album view showing uploaded photos
A demo album. These are Wikimedia Commons wedding photographs uploaded to a test album — I did not screenshot anyone's real album, because they are private.

Knowing when someone uses it

I wanted to know the moment a real person creates an album, without giving myself a way to snoop on their photos. So there's an owner page listing albums — titles, dates, photo counts, and links — gated behind an encrypted secret, which returns 404 rather than 403 when the secret isn't configured, so its existence isn't advertised. A Telegram bot messages me when an album is created.

Tadam album management page
The management view the couple gets: share links, the QR sheet, the ZIP download, and per-photo delete.

That page hung the first time it met a realistic amount of data. With around two hundred albums it was making R2 calls one after another and taking long enough that I assumed it had crashed. A concurrency pool of twelve brought it to 0.7 seconds, and photo counts are only computed for the newest forty albums.

Details I'm glad I spent time on

What I'm not claiming

I was tempted to put "the first and only service of its kind in Uzbekistan" on the homepage. I didn't, because I haven't checked every competitor in the country and I can't prove it. It would probably have converted better. It would also have been a claim I'd have to defend the first time somebody found a competitor, and the whole point of building things under my own name is that everything on them holds up.

Here's the honest status: tadam.uz is live, hardened, fast, and has not yet been used at a single real wedding. That's the next thing to fix, and it isn't a code problem.

← ALL ENTRIES · RSS · © 2026 Maqsudjon Polatov