PROJECT STUDY

Rukisa

Guests scan and send. The couple keeps the originals.

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.

Explore the build
Rukisa illustration: a table card with the couple's names and a QR code, a phone sending four photos with per-photo saved and progress states, and the couple's dashboard with a download-originals button, filter chips and a photo grid.
Original interface illustration · Synthetic demo data.View larger

At a glance

Industry
Events
Capabilities
6
Technologies
12
Year
2026
94/94

Automated tests passing against PostgreSQL

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

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Stack by layer

Frontend
  • Next.js
  • React
  • TypeScript
  • CSS Modules
Backend
  • Node.js
  • Next.js Route Handlers
  • Server Actions
  • Postgres.js
  • sharp
  • yazl
  • uqr
Data
  • PostgreSQL
  • SQL migrations
  • Local disk object storage
Infrastructure
  • pnpm workspaces
  • Docker Compose
  • Node.js test runner
  • ESLint
  1. 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

  2. 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

  3. 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

  4. 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

  5. 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 palette

  • Forest#232a24Headings and the couple's names, primary buttons, focus rings, selected states and the printed QR
  • Ivory#f5f0e7Page background and labels on forest buttons
  • Champagne#d8c7a9The hairline under the couple's names, and accents on dark forest sections
  • Parchment#e9e1d3Quiet surfaces for the guest-link panel, notices, count chips and photo placeholders
  • Deep ink#25302aBody copy and interface labels
  • Muted evergreen#58695bSecondary text, eyebrows and captions
  • Error ink#9c4037Failed-upload captions, form errors and the outlined delete button
  • 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 and events work, or talk to us about your product.

talk to us about your product

Let’s connect the dots

Let's put these ideas to work.

Share a process, product, or problem you want to improve. We will work through the possibilities together.

Discuss your project