Skip to case study
Parth AkholkarLet's talk
04Consumer print2026Live

Pixibooks

A B2C memory-book studio. Photos in, print-ready PDFs out. The hard parts: tab-kill recovery, HEIC fallback, deterministic 8-template layout.

Client
Pixign (client work via Feyntech.AI)
Role
Full-stack builder
Stack
Next.js · Supabase · react-pdf
Pixibooks product interface

Snapshot

A B2C memory-book studio for Pixign (Ahmedabad). Photos in, print-ready PDFs out. The product itself is small - upload, an auto-layout engine, a mobile-friendly customiser, a PDF export - but every part of it is a recovery surface for things mobile browsers do badly.

Context

Pixign sells printed memory books and wanted a self-serve studio so customers could lay their own albums out without a designer in the loop. The old flow was email-attached photos and Photoshop. The new flow had to work on a phone, survive iOS Safari's memory limits, deduplicate photos across sessions, and end in a press-ready PDF.

Client work via Feyntech.AI. Shipped on Fly.io, Supabase for auth + storage + Postgres.

Problem

The hard parts weren't the layout engine or the PDF generation. They were everything around them:

  • iOS Safari kills the tab when the photo set is large. Without intervention the user reloads into the upload modal and loses session context - even though the work is auto-saved server-side.
  • HEIC files from iPhones don't decode in every browser. Tiled HEVC variants don't decode in any of them client-side.
  • Customers re-upload the same photos across sessions; without dedupe, the storage bill compounds.
  • A two-pass thumbnail strategy is needed because doing image work in API routes is expensive enough to be visible.

Approach

Tab-kill recovery. The studio writes a small heartbeat to sessionStorage every five seconds - {bookId, at}. On the next /create mount, if the heartbeat is fresh (under 60s), the page treats the load as a tab kill, re-fetches the book, and routes the user back into the studio on the book they were editing. Don't clear it on unmount; the time-staleness check is the only signal that distinguishes a clean reload from a kill.

HEIC fallback. Client tries heic2any first; on failure, a server route decodes through sharp. The fallback path catches the iPhone HEVC variants the client library can't.

Photo dedupe. Every photo is SHA-256'd client-side. The photos table is unique on (user_id, sha256). Re-uploads land on the existing row.

Two-pass thumbnails. The proxy at /api/books/photo?id=… resolves photos through a fast path (a 512px JPEG written client-side at upload time, stored alongside the original) and falls back to a sharp lazy-resize on miss, deduping concurrent first-fetches. As the fast-path drains over time, the lazy fallback becomes deletable.

AutoFlowEngine. A deterministic function that maps a photo set to spread templates (Full Page, Side by Side, Stacked, Big + Small, 4 Grid, 6 Grid, 9 Grid) based on count + orientation, with a prime-7 stride to scatter similar templates. Re-runs are idempotent for the same input; per-slot transforms are stored separately so user pans survive re-runs.

Tech

  • Next.js 14 (App Router), React 18, TypeScript
  • Supabase (Auth + Postgres + Storage), Drizzle ORM
  • Custom hash-tracked migration runner - migrations are immutable once applied
  • pdf-lib + @pdf-lib/fontkit + @react-pdf/renderer + @resvg/resvg-js for the print pipeline
  • sharp server-side for image work
  • Fly.io standalone Docker, region bom

What I learned

Most of the engineering in a "simple" creative tool is recovery. The layout engine is 143 lines; the tab-kill heartbeat, the upload queue's exponential backoff, the thumbnail proxy, and the migration runner are each about as long, and each one fixes a class of problem you don't see until users hit it on a phone in a bad-network coffee shop.