# Rukisa

> Wedding photo sharing: guests scan one QR and send photos with no app or account, and the couple curates the verified originals in a private gallery, newest first.

Guests scan and send. The couple keeps the originals.

- URL: https://weaveward.com/work/rukisa-wedding-photo-sharing
- Year: 2026
- Industry: Events
- Capabilities: Web apps
- Technologies: Next.js, React, TypeScript, PostgreSQL, Postgres.js, sharp, yazl, uqr, Node.js, CSS Modules, Docker Compose, Node.js test runner

## The challenge

The most candid wedding photos are taken by guests, and they end up scattered across phones and chat groups, often compressed on the way. Couples want every original in one private place, but guests will not install an app, create an account or wait on a weak venue connection to share them.

Trust matters as much as convenience. A guest needs to know that a photo really arrived before closing the page, and a couple needs to know that the archive they download holds exactly what was sent.

## What we built

Rukisa is a mobile-first web product built around one communal QR code: a guest capture page, a private couple dashboard and an editorial landing page, all written in Indonesian.

- **One QR, no app or account.** Guests scan the communal QR, take a photo or pick from their gallery, and send it without a login, a name or an RSVP
- **Saved means verified.** Each photo shows its own progress, and "Tersimpan" (saved) appears only after the server has checked the stored bytes. A failed photo can be retried on its own or together with every other failed photo
- **A private couple gallery.** Couples create events, download the guest QR as an SVG for printing, and browse verified photos newest first, with WebP thumbnails and previews stripped of EXIF and GPS data
- **Curation.** Photos can be favourited, hidden or permanently deleted, one at a time or in bulk. Hidden photos stay with the couple and are excluded from anything a guest could see
- **Originals in one download.** A ZIP of all photos or of favourites holds the untouched originals with a SHA256SUMS.txt manifest, built in the background with real progress and downloadable for 24 hours
- **Guest links under control.** The couple can rotate the capture code so an old QR stops at once, close uploads, or set an upload window in WIB, WITA or WIT

## How it works

### Reserve, upload, verify

The guest's browser reserves an upload and receives a server-generated key with a 15-minute HMAC-signed URL bound to the exact size and type. It sends the original bytes straight to that URL, and a finalize call re-reads the stored object, checks its size and image signature and records its SHA-256 before the photo counts.

### Queue the follow-up work

Finalize marks the photo verified and queues its preview job in the same PostgreSQL transaction. Two in-process workers share a PostgreSQL job table: a media lane for WebP previews, deletion cleanup and expiry, and an export lane for ZIP builds, so the product runs without Redis or a separate queue service.

### Scope every read

A host access key is exchanged for an HMAC-signed, HttpOnly session cookie scoped to the host. Every dashboard query and media route then checks the host, the event and the photo's verified state in that order and answers 401 or 404 outside that chain, and every page is served noindex with a same-origin referrer policy.

### Stream the originals

An export snapshots its photos, confirms that every original is present at its verified size and that free disk covers the archive, then streams the originals with yazl in STORE mode so the bytes go in unchanged. The download route re-checks the session, the event, the expiry and any deleted photos on every request, including resumed byte ranges.

## Engineering insights

The product's promise is that saved means saved, so every state a guest or a couple sees is backed by a check on the stored bytes.

- **Saved only after the server checks the bytes.** The storage driver writes through a temp file, never overwrites an existing object and discards a body of the wrong size. Finalize accepts only the declared size and a JPEG, PNG, WebP or HEIC/HEIF signature, and it is idempotent across processes through a conditional update on the pending row
- **A verified photo and its job commit together.** Because the verified state and the preview job share one transaction, a crash cannot leave a verified photo without queued work. Workers claim jobs with FOR UPDATE SKIP LOCKED under a five-minute lease, and every state write checks the worker's lock, so a worker that lost its lease cannot overwrite the result
- **Deleted only after storage is re-checked.** A deletion moves photos out of the verified state in one SQL statement, so access ends at once, and a cleanup job removes the original and every derivative, then re-checks each key before writing a tombstone without file name or checksum. Preview jobs re-check the photo under a lock before saving, so a deleted preview is never recreated
- **Every ZIP entry matched to its upload.** Each original is hashed against the SHA-256 recorded at upload as it streams into the archive, so a missing or altered original fails the export with a named reason and leaves no partial file. The archive carries a SHA256SUMS.txt manifest and switches to ZIP64 when it needs to
- **The guest link can add photos, never read them.** The communal QR carries a capture code built from the couple's names and 40 random bits. It can only reserve and finalize uploads for its own event and returns no photos, and hidden or deleted photos are filtered out inside the database by a guest-visible view

## Design system

Quiet Evergreen / Ivory Film treats the product as an art-directed wedding journal rather than a software dashboard: forest, ivory and champagne anchor plain CSS custom properties, hairlines replace cards and the photographs carry the colour, for Indonesian couples and their guests, older guests and weak venue connections included.

- **Colour.** Forest anchors headings, the couple's names, primary buttons and focus rings on an ivory page. Champagne draws the hairline under the names, parchment holds the quiet panels, and a dedicated error ink marks failed uploads and form errors
- **Typography.** No web fonts load, so pages stay light on venue connections: a platform serif stack sets the couple's names and headings, and the system sans sets body copy, forms and buttons. Guest body text is 16 px with an 18 px lead, and frame numbers and counts use tabular numerals
- **Components.** Everything is in-house, with no UI kit, icon pack or animation library: global button, form, chip and focus classes, CSS Modules per feature, seven inline SVG icons always paired with text or an accessible label, a server-rendered QR code and a native dialog for permanent deletion that focuses the cancel button first
- **Accessibility.** Primary controls are 52 px tall and every touch target is at least 44 px, focus stays visible, motion respects reduced-motion settings, and each state offers one obvious primary action

## Where this fits

Any product that collects files from many people, from event photos to site inspections, needs the same two guarantees: almost no effort for the sender and verified originals for the owner. Explore our [web apps](/services/web-apps) and [events work](/industries/events), or [talk to us about your product](/contact).
