Get started
Engineering· Flickks Engineering

From upload to unlock, how Flickks runs on Cloudflare

Follow a single photo from a photographer's upload to a guest's screen, and meet every Cloudflare piece that touches it along the way.

Two people touch every photo on Flickks, and they could not be more different. A photographer needs a real application: sign in, upload, organize, price, publish. A guest needs one fast page of photos, usually on a phone, with no account and nothing to install.

This post follows a single photo through that whole trip: from the moment a photographer drags it into an album, to the moment a guest’s browser paints it on screen. Along the way we will meet every Worker, bucket, and database that touches it, and see why the guest’s half of the trip barely touches a server at all.

The cast

A quick reference, since the same few names show up at every stage below.

Name Role
Dashboard app Photographer SPA on Pages
API Worker Auth, albums, billing, publish
Tenant site Published guest-facing site
Site Worker Unlock, unlocked galleries, custom domains
Media CDN Theme assets and photos
Photo bucket R2 for sites, themes, media, protected originals
Activity inbox KV for unread photographer notifications
Support Worker Dashboard chat to Slack bridge
Support bucket Chat attachments

Every diagram below follows the same layout: request on the left, Worker or storage in the middle, bindings on the right. Click one to expand it; the URL updates so the view can be shared directly.

Stage 1: the photographer signs in and uploads

The photographer’s half of Flickks lives on the dashboard app, a single-page app on Cloudflare Pages. Sessions stay on that origin, and every action, signing in, uploading, organizing, pricing, goes to the API Worker behind it.

The API Worker is the only thing that touches D1 and the photo bucket directly:

  • Accounts: session auth on the API Worker, with bot checks on sensitive requests.
  • Albums and photos: metadata in D1, files streamed into the photo bucket. Flipping an album from public to password moves its objects between the open media area and a protected prefix.
  • Billing: the API starts checkout with an external payment provider and updates plan entitlements in D1 when a webhook arrives.
  • Activity: the activity inbox in KV holds unread events for the photographer, like a new comment or a completed order.

Loading diagram…

The dashboard app talks to the API Worker, which stores accounts and albums in D1, files in the photo bucket, and unread events in KV.

Stage 2: hitting publish

Publish is where a photographer’s private workspace turns into a public website. There is no separate build queue: the same API Worker that handled the upload renders the album and profile pages and writes static HTML straight into the photo bucket.

Every photographer is a tenant, and a tenant’s public surface is a folder: sites/{username}/…. By default that folder is reachable at a subdomain, the hostname is the handle, and custom domains resolve the same way once DNS points at Flickks, through a hostname-to-user lookup in D1.

Tenants do not get their own Workers, databases, or buckets. Isolation comes from key prefixes and hostnames instead:

  • Published HTML: sites/{username}/…
  • Public media: under an open media prefix
  • Password-album originals: under a protected prefix, protected/{userId}/{albumId}/…
  • Theme packs: stored once per theme, never copied per tenant

A short list of reserved subdomains (www, cdn, api, and similar) cannot be claimed as a handle, so infrastructure hosts never collide with a real tenant.

A password album publishes too, just as a gate page: no grid, no cover image, and marked so search engines skip it. The gate is static HTML like everything else. The real gallery only renders after someone unlocks it, which is Stage 4.

This is where the trip gets cheap. A guest opening a public profile or a public album is not waking a Worker at all: their browser’s request for alice.flickks.com/ resolves straight to an object in the photo bucket, sites/alice/index.html, while theme styles and public photos load from the media CDN alongside it.

The Site Worker stays out of this path entirely for unprotected content. It only gets involved for the handful of cases that need state:

  • Password-album unlock and unlocked gallery views
  • Custom domains, where the hostname-to-user mapping in D1 has to be resolved

Loading diagram…

Unprotected pages come straight from the photo bucket. Unlock requests and custom domains go through the Site Worker.

Stage 4: a guest hits a locked door

If the album a guest wants is password-protected, the static gate page they land on asks for a password instead of showing photos. Submitting it is the first request that actually reaches the Site Worker.

The Worker checks the password against a slow hash and, on success, sets an HttpOnly unlock cookie good for about a week. That cookie is tied to when the password was last changed, so changing the password invalidates every session that unlocked under the old one. From here, reloading the album with the cookie attached gets a fully rendered gallery instead of the gate.

Loading diagram…

A locked album opens as a static gate from the bucket. Submitting the password hits the Site Worker, which sets an unlock cookie and later renders the gallery.

Stage 5: loading the actual photos

An unlock cookie proves the guest knew the password, but it does not hand them the photos directly. Password media never touches the open media path, it lives under the protected prefix in the photo bucket, and the only way to reach it is a signed media link.

After unlock, the Site Worker (or the API Worker, when the photographer previews their own album) mints a timed HMAC token compatible with Cloudflare’s is_timed_hmac_valid_v0 WAF rule on the media CDN. We sign the album folder prefix rather than each file, so one token covers /protected/{userId}/{albumId}/… for about three hours, and every image tag on the unlocked page uses those signed URLs directly. The edge rejects any protected object without a valid signature, and public bucket access stays off so the WAF check cannot be bypassed.

So the full lock, start to finish, is three layers: a static gate page, an unlock cookie, and HMAC-signed URLs that authorize the actual bytes. Once the cookie expires, the guest is back at the gate.

Stage 6: the theme behind the scenes

None of the pages a guest sees are built from scratch per photographer. Themes compile down to shared packs in the photo bucket: shells, tokens, and assets rewritten to load from the media CDN. A photographer just stores a theme id, and publish fills the matching pack with their profile and albums to produce their tenant site. Guests never download a full application, just static HTML and a handful of assets. When a theme changes, we rebuild its pack once, and existing sites pick up the update on their next publish.

Loading diagram…

Themes compile into shared packs in the photo bucket. Publish fills a pack with the photographer's data and writes the tenant site.

Off to the side: when a photographer needs help

Not every stage of the trip is about a guest. The dashboard has a chat widget, and the Support Worker carries that conversation into Slack. On a photographer’s first message, Flickks opens a private channel for them, and messages and replies sync in both directions. Chat images land in a separate support bucket, so support traffic never touches customer media storage. Staff work from Slack; photographers never leave the dashboard.

Loading diagram…

Dashboard chat goes through the Support Worker into Slack. Attachments land in a separate support bucket, not the photo bucket.

The whole trip, in one picture

Zoom out and the journey has one shape. A photographer’s upload goes through the API Worker into D1 and the photo bucket. Publish turns that into static objects at the tenant’s own prefix. A guest’s browser reads those objects straight from the bucket for anything public. The Site Worker only wakes up for unlock, unlocked galleries, and custom domains, and even then it hands the guest back to static, HMAC-signed URLs as soon as it can.

Loading diagram…

Upload and publish go through the API Worker into D1 and the photo bucket. Guests read public pages from the bucket; unlock wakes the Site Worker.

Publish once. Serve static whenever possible. Wake a Worker only when the request genuinely needs state.