Guests scan a QR code, type their name, and upload photos, videos, or voice messages. Files go straight to a Google Drive folder you own; metadata lands in Cloudflare D1. Runs entirely on Cloudflare's free tier.
- Upload page: photos (≤ 25 MB), videos (≤ 50 MB), voice notes (≤ 3 min), written messages.
- Offline-resilient queue — blobs persist in IndexedDB across tab closes, browser restarts, and long disconnects; auto-resumes when the connection returns.
- Shared gallery with infinite scroll, kind filters, 30-second live refresh, and a lightbox with download/share (Web Share API on mobile,
<a download>fallback on desktop). - Admin page for hiding or deleting entries; deletion removes the file from Drive too.
- Per-table QR codes (
?table=7auto-tags uploads). - Gallery thumbnails served by Google's CDN — zero Worker traffic per image load.
Everything fits in free tiers for a typical one-day event.
| Free tier | |
|---|---|
| Cloudflare Pages + Functions | 100k req/day |
| Cloudflare D1 | 5 GB, 25M reads/day |
| Google Drive | 15 GB |
npm install
All names, dates and colours live in one git-ignored file:
cp public/config.example.js public/config.js
export const WEDDING = {
heading: { pl: "Wesele Anny i Jana", en: "Anna & Jan's Wedding" },
eventDate: { pl: "12.09.2026", en: "12.09.2026" },
lead: { pl: "Podziel się zdjęciem…", en: "Drop in photos…" },
pwaName: "Wesele Anny i Jana",
pwaShortName: "Wesele A&J",
pwaDescription: "Podziel się wspomnieniem z wesela…",
themeColor: "#6d7c3d",
};heading is the full string per language rather than a name that gets
interpolated — Polish needs the genitive, which can't be derived from the
nominative.
public/manifest.webmanifest is generated from this file by
scripts/gen-config-assets.mjs, which runs automatically before dev, test,
qr and deploy (or by hand: npm run gen). Both config.js and the
generated manifest are git-ignored, so a fork stays anonymous; if config.js
is missing, the generator seeds it from config.example.js so tests and a
fresh clone still boot.
Deploy from local, not from a GitHub-connected Pages build — a build
triggered from the repo would not have config.js.
- console.cloud.google.com → new project → enable Google Drive API.
- OAuth consent screen → External → scope:
drive.file→ Publish app (stays in Production so tokens don't expire after 7 days). - Credentials → Create OAuth client ID → Desktop app → download JSON as
oauth-client.json(git-ignored). - Create a Drive folder for uploads; copy its ID from the URL.
npm run auth
Browser opens, you approve, terminal prints the refresh token.
npx wrangler login
npx wrangler d1 create wedding-photos # paste the returned database_id into wrangler.toml
npm run db:init
Set secrets:
npx wrangler pages secret put GOOGLE_CLIENT_ID
npx wrangler pages secret put GOOGLE_CLIENT_SECRET
npx wrangler pages secret put GOOGLE_REFRESH_TOKEN
npx wrangler pages secret put DRIVE_FOLDER_ID
npx wrangler pages secret put ADMIN_TOKEN # any random 32-char string
npm run db:init:local
npm run dev
Open http://localhost:8788.
npm run deploy
node scripts/make-qr.mjs https://your-app.pages.dev # one general QR
node scripts/make-qr.mjs https://your-app.pages.dev 1 10 # + one per table
Output lands in qr-output/ as PNG and SVG. Use SVG for print.
/admin.html?token=<ADMIN_TOKEN> — hide entries from the gallery or delete them (and the Drive file) permanently.
Drive files are granted anyone with link: reader so Google's CDN can serve gallery thumbnails. "Hide" removes an entry from the feed but does not revoke the Drive link — use Delete to fully remove content.
The admin token travels in the query string and will appear in browser history and server logs. The backend also accepts an x-admin-token header if you want to avoid that.
Playwright e2e suite covering upload, offline queue, gallery, lightbox, and download. No Cloudflare credentials needed — API calls are mocked, and pretest seeds a placeholder public/config.js if you don't have one.
npx playwright install chromium # one-time
npm test
npm run test:ui # opens the UI runner
Specs live in tests/e2e/. Failing runs keep a trace at test-results/<name>/trace.zip.
MIT