Skip to main content

Photobooth Web App - Technical Documentation

Version: 1.0.0
Last Updated: February 2026
Status: Production-ready


Table of Contents

  1. Executive Summary
  2. Technology Stack
  3. Architecture Overview
  4. Project Structure
  5. Application Flow
  6. Core Components
  7. State Management
  8. Services & Utilities
  9. PWA & Offline Support
  10. Configuration & Settings
  11. Testing
  12. Build & Deployment
  13. Future Considerations (ADRs)

Executive Summary

This is a Next.js-based photobooth web application designed for event photography. It enables users to:

  • Select photo frame templates
  • Capture photos via camera or upload from gallery
  • Reorder and edit images
  • Generate printable photo collages with custom frames
  • Print directly via Bluetooth to Niimbot thermal printers

The application is built as an offline-capable PWA that can run on kiosks or tablets at events without reliable internet connectivity.

The repo also includes a lightweight backend for:

  • customer records
  • payment creation and webhook handling
  • subscription lifecycle tracking
  • entitlement-based license verification

Technology Stack

Core Framework

  • Next.js 16.x - React framework with App Router support
  • React 18.x - UI library
  • TypeScript 5.x - Type safety

Styling & UI

  • Tailwind CSS 3.x - Utility-first CSS framework
  • Radix UI - Accessible component primitives
    • @radix-ui/react-dialog
    • @radix-ui/react-select
    • @radix-ui/react-slider
    • @radix-ui/react-switch
    • @radix-ui/react-tabs
  • Framer Motion - Animation library
  • Lucide React - Icon library
  • clsx & tailwind-merge - Class utilities

State Management

  • React Context API - Built-in state management
  • Session Storage - Ephemeral image data
  • Local Storage + IndexedDB - Persistent settings

PWA & Service Workers

  • @serwist/next - Service worker integration for Next.js
  • @serwist/precaching - Precaching for offline support

Bluetooth Printing

  • @mmote/niimbluelib - Niimbot thermal printer Bluetooth library

Drag & Drop

  • @dnd-kit/core - Drag and drop primitives
  • @dnd-kit/sortable - Sortable list components

QR Code

  • qrcode - QR code generation
  • html5-qrcode - QR code scanning

Fonts

  • @fontsource/inter
  • @fontsource/lato
  • @fontsource/montserrat
  • @fontsource/open-sans
  • @fontsource/roboto

Development & Build

  • ESLint - Code linting
  • Prettier - Code formatting
  • Playwright - E2E testing
  • Wrangler - Cloudflare Pages deployment

Architecture Overview

Billing and Access Architecture

The backend now separates commercial access into four domains:

  • payments for transaction records and provider status
  • subscriptions for recurring lifecycle state
  • entitlements as the source of truth for access windows
  • licensing for stable public API responses such as /api/v1/license/verify/:key

This allows the app to support both:

  • one-time activation with perpetual entitlement
  • monthly subscriptions with recurring entitlement windows

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│ Next.js Application │
├─────────────────────────────────────────────────────────────┤
│ Pages Router (Legacy) │ App Router (New) │
│ - /pages/*.tsx │ - /app/* │
├─────────────────────────────────────────────────────────────┤
│ React Context Providers │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Settings │ │ Printer │ │ Voucher │ │
│ │ Provider │ │ Provider │ │ Provider │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Collage │ │
│ │ Provider │ │
│ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Services Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Storage │ │ Image │ │ Bluetooth │ │
│ │ (IndexedDB) │ │ Processing │ │ Printing │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Service Worker (Serwist) │
│ - Precaching for offline │
│ - Runtime caching strategies │
└─────────────────────────────────────────────────────────────┘

Key Architectural Decisions

  1. Pages Router: Uses Next.js Pages Router (not App Router) for simpler integration with Serwist service worker
  2. React Context: Lightweight state management without external libraries like Redux
  3. Client-Side Rendering: Most pages use client-side features (Bluetooth, camera, storage)
  4. Hybrid Storage: IndexedDB with localStorage fallback for settings persistence

Project Structure

photobooth-web-app/
├── public/ # Static assets
│ ├── images/ # Frame images, icons, animations
│ ├── manifest.json # PWA manifest
│ └── offline.html # Offline fallback page
├── src/
│ ├── app/ # App router components (newer)
│ │ ├── customization/ # Customization page components
│ │ │ ├── components/ # Tabs, forms, inputs
│ │ │ ├── constants.ts # Default settings constants
│ │ │ └── types.ts # TypeScript interfaces
│ │ ├── image-editing/ # Image editing components
│ │ │ ├── components/ # EditMode, PreviewMode, SortableItem
│ │ │ └── hooks/ # useImageItems, useImageReorder, usePreviewMode
│ │ ├── image-upload/ # Image upload components
│ │ │ ├── components/ # VideoPreview, ImageThumbnails
│ │ │ └── hooks/ # useImageCapture, useImageUpload
│ │ ├── frame-selection/ # Frame selection components
│ │ └── globals.css # Global Tailwind styles
│ ├── components/ # Shared components
│ │ ├── ui/ # Reusable UI components (Radix-based)
│ │ ├── shared-layout.tsx # Page layout wrapper
│ │ ├── voucher-scan.tsx # QR code scanner
│ │ └── ...
│ ├── data/ # Static data
│ │ └── frames.json # Frame templates
│ ├── hooks/ # Custom React hooks
│ │ ├── useCamera.ts # Camera access
│ │ ├── useOffline.ts # Offline detection
│ │ ├── useInstallPrompt.ts # PWA install prompt
│ │ ├── useFrameConfig.ts # Frame configuration
│ │ └── useImageDimensions.ts
│ ├── lib/ # Utility libraries
│ │ ├── storage.ts # IndexedDB + localStorage
│ │ ├── image-processing.ts # Canvas-based image processing
│ │ ├── utils.ts # General utilities
│ │ ├── hooks.ts # Storage hooks
│ │ └── fonts.ts # Font loading
│ ├── pages/ # Pages (Pages Router)
│ │ ├── index.tsx # Home page
│ │ ├── frame-selection.tsx
│ │ ├── image-selection.tsx
│ │ ├── image-upload.tsx
│ │ ├── image-editing.tsx
│ │ ├── image-final.tsx
│ │ ├── customization.tsx # Settings page (PIN-protected)
│ │ ├── offline.tsx # Offline page
│ │ ├── _app.tsx # App wrapper with providers
│ │ └── _document.tsx # HTML document
│ ├── providers/ # React Context providers
│ │ ├── SettingsProvider.tsx # App settings (persisted)
│ │ ├── PrinterProvider.tsx # Bluetooth printing
│ │ ├── CollageProvider.tsx # Image collage generation
│ │ └── VoucherProvider.tsx # Voucher/PIN access
│ └── sw.ts # Service worker
├── tests/ # Playwright tests
│ └── e2e/
│ ├── flows/ # User flow tests
│ └── visual/ # Visual regression tests
├── docs/ # Architecture decision records
├── next.config.mjs # Next.js configuration
├── tailwind.config.ts # Tailwind configuration
├── tsconfig.json # TypeScript configuration
├── Dockerfile # Docker build
└── package.json # Dependencies

Application Flow

Main User Flow

┌──────────────┐     ┌──────────────────┐     ┌───────────────────┐
│ HOME │────▶│ FRAME SELECTION │────▶│ IMAGE SELECTION │
│ (index) │ │ (frame-id) │ │ (camera/gallery) │
└──────────────┘ └──────────────────┘ └───────────────────┘
│ │
│ ▼
│ ┌───────────────────┐
│ │ IMAGE UPLOAD │
│ │ (capture/select) │
│ └───────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ HOME │◀────│ IMAGE FINAL │◀────│ IMAGE EDITING │
│ (restart) │ │ (print/preview) │ │ (reorder/filter) │
└──────────────┘ └──────────────────┘ └───────────────────┘

Detailed Page Flow

  1. Home Page (/)

    • Entry point with customizable start button
    • Shows "Connect Printer" button if printer enabled
    • Clears previous session verification
  2. Frame Selection (/frame-selection)

    • Displays available frame templates (1-4 photos)
    • Shows voucher scan if voucher system enabled
    • Passes frame ID to next page
  3. Image Selection (/image-selection)

    • Choose between camera or gallery upload
    • Detects camera availability
  4. Image Upload (/image-upload)

    • Camera mode: Live preview with countdown capture
    • Gallery mode: File picker for local images
    • Supports multiple images based on frame selection
    • Stores images in sessionStorage
  5. Image Editing (/image-editing)

    • Drag-and-drop reordering via @dnd-kit
    • Preview mode with brightness adjustment
    • Atkinson dithering filter for thermal printing
    • Auto-enters preview mode for single images
  6. Image Final (/image-final)

    • Displays final collage with frame applied
    • Auto-print if printer connected and enabled
    • Manual print/reprint button
    • Countdown timer to auto-reset (120s)
    • "Done" button returns to home
  7. Customization (/customization)

    • PIN-protected settings page (default: 1234)
    • Tabs: Page, Frame, Button, Printer, Voucher, Security
    • Settings persisted to IndexedDB/localStorage

Core Components

Layout Components

ComponentPurpose
SharedLayoutCommon page wrapper with header, back button, content area
PageTransitionFramer Motion page transitions

UI Components (Radix-based)

ComponentDescription
ButtonCustomizable button with theme support
DialogModal dialogs
SelectDropdown selection
SliderRange input (brightness control)
SwitchToggle (on/off settings)
TabsTabbed interface for customization

Feature Components

ComponentPurpose
FrameFrame preview with settings applied
VideoPreviewCamera live preview + capture
ImageThumbnailsMulti-image slot display
VoucherScanQR code scanner for voucher validation
LockWidgetPIN entry overlay for customization access
OfflineIndicatorNetwork status indicator
InstallPromptPWA installation prompt

State Management

Provider Architecture

<App>

├─ SettingsProvider
│ └─ CustomizationSettings (persisted)
│ ├─ PageSettings
│ ├─ FrameSettings
│ ├─ ButtonSettings
│ ├─ SecuritySettings (PIN)
│ ├─ PrinterSettings
│ └─ VoucherSettings

├─ PrinterProvider
│ ├─ Bluetooth client state
│ ├─ Connection status
│ └─ Print functions

├─ VoucherProvider
│ ├─ Verification state
│ └─ Verify function

└─ CollageProvider
├─ Final image (base64)
├─ Brightness value
└─ Generate/clear functions

Settings Provider

File: src/providers/SettingsProvider.tsx

Manages all application settings with persistence:

  • Uses useLocalStorage hook with IndexedDB + localStorage fallback
  • Supports settings migration for schema updates
  • Default values defined in provider

Stored Settings:

  • pageSettings: Background, title, subtitle
  • frameSettings: Header, footer, background, photo frame styling
  • button: Start button text, colors, border styling
  • security: PIN code for admin access
  • printer: Enable/disable, default copies, max copies, paper size
  • voucher: Enable/disable, passcode for voucher access

Printer Provider

File: src/providers/PrinterProvider.tsx

Handles Bluetooth communication with Niimbot printers:

  • Connect/disconnect via Web Bluetooth API
  • Print images with configurable copies
  • Supports E2E testing mode (NEXT_PUBLIC_E2E=true)
  • Enforces max copies limit from settings

Voucher Provider

File: src/providers/VoucherProvider.tsx

Manages voucher/PIN-based access control:

  • Validates passcode from QR scan
  • Stores verification state in sessionStorage
  • Clears on new session start

Collage Provider

File: src/providers/CollageProvider.tsx

Generates final printable image:

  • Combines multiple images into collage based on frame settings
  • Applies Atkinson dithering for thermal printer output
  • Supports brightness adjustment

Services & Utilities

Storage Service

File: src/lib/storage.ts

Dual-layer persistent storage:

  1. IndexedDB - Primary storage (PWA-reliable)
  2. localStorage - Fallback, synced with IndexedDB

Key Functions:

  • getItem(key) - Read from storage
  • setItem(key, value) - Write to storage
  • removeItem(key) - Remove a persisted value
  • clear() - Clear all persisted values

Image Processing

File: src/lib/image-processing.ts

Canvas-based image manipulation:

Functions:

  • generateCollage(images, settings, width, height) - Creates combined image
  • atkinson(imageData, threshold) - Applies Atkinson dithering
  • adjustBrightnessContrastGamma() - Print compensation
  • calculateCropCoordinates() - Aspect ratio preservation

Collage Layouts:

  • 1 image: Full area
  • 2 images: Vertical split (top/bottom)
  • 3 images: 2 top, 1 bottom centered
  • 4 images: 2x2 grid

Custom Hooks

HookPurpose
useCameraCamera stream management
useImageCapturePhoto capture to canvas
useImageUploadGallery file handling
useImageDimensionsTarget dimensions calculation
useFrameConfigURL param parsing for frame
useOfflineOffline detection
useInstallPromptPWA install handling

PWA & Offline Support

Service Worker Configuration

File: src/sw.ts (compiled to public/sw.js)

Uses Serwist for Next.js service worker integration:

Caching Strategies:

Request TypeStrategyCache Name
Static assets (images, fonts)Cache-firststatic-assets-v1
API/JSONCache-firstapi-cache-v1
Navigation (pages)Cache-firstpages-cache-v1

Offline Behavior:

  • All app pages pre-cached
  • Falls back to /offline.html for navigation failures
  • Assets cached on first load

PWA Manifest

File: public/manifest.json

  • App name: "Photobooth"
  • Standalone display mode
  • Theme color: Black
  • App icons: 48x48 to 512x512

Offline Pages

  • /offline - React component for offline state
  • /offline.html - Static fallback HTML

Configuration & Settings

Default Settings

All default values are defined in src/providers/SettingsProvider.tsx:

// Default button
{
text: "Mulai Sesi",
backText: "Kembali",
bgColor: "#000000",
textColor: "#ffffff",
borderRadius: 8,
font: "Default",
fontSize: "1rem"
}

// Default printer
{
active: true,
name: "NIIMBOT",
defaultCopies: 1,
maxCopies: 10,
paperWidthMM: 30,
paperHeightMM: 50,
dpi: 300
}

// Default security
{
pin: "1234",
iconSize: 24,
iconOpacity: 0
}

Frame Templates

File: src/data/frames.json

Static frame definitions with IDs mapping to photo counts:

  • 1 → 1 photo
  • 2 → 2 photos
  • 3 → 3 photos
  • 4 → 4 photos

Testing

Test Structure

tests/
├── e2e/
│ ├── flows/
│ │ ├── photobooth.spec.ts # Main flow tests
│ │ ├── customization.spec.ts # Settings tests
│ │ ├── voucher.spec.ts # Voucher flow tests
│ │ ├── pin.spec.ts # PIN access tests
│ │ └── offline.spec.ts # Offline tests
│ ├── visual/
│ │ └── snapshots.spec.ts # Visual regression tests
│ ├── fixtures/
│ │ └── test-image.png # Test image fixture
│ └── README.md

Running Tests

# All E2E tests
npm run test:e2e

# Visual snapshot tests (iPad)
npm run test:e2e:visual

# UI mode (interactive)
npm run test:e2e:ui

# Debug mode
npm run test:e2e:debug

Test Configuration

  • Uses Playwright with Chromium/iPad emulation
  • Base URL: http://localhost:3000
  • E2E mock mode available via NEXT_PUBLIC_E2E=true

Build & Deployment

Local Development

# Install dependencies
npm install

# Start dev server (Turbo mode)
npm run dev

# Lint
npm run lint

# Format
npm run format

Production Build

# Build for production
npm run build

# Start production server
npm run start

Deployment Options

1. Cloudflare Pages (Primary)

npm run deploy

Deploys to Cloudflare Pages using Wrangler.

Configuration: wrangler.toml (implicit)

2. Docker

# Build image
docker build -t photobooth-web-app .

# Run container
docker run -p 3000:3000 photobooth-web-app

Environment Variables

VariablePurposeDefault
NODE_ENVEnvironmentdevelopment
NEXT_PUBLIC_E2EE2E test modefalse

Future Considerations (ADRs)

ADR-001: QR Code Download Feature

Status: Proposed

Decision: Use Supabase for cloud image storage with QR code download.

Trade-offs:

  • ✅ Excellent DX, rapid implementation, free tier
  • ⚠️ Egress cost risk (2GB/month limit on free tier)
  • ✅ Scalable architecture path available

Alternative: Cloudflare R2 (no egress fees) - Re-evaluate if traffic exceeds free tier

ADR-002: Vendor Access Control

Status: Proposed

Decision: Remote validation server with Cloudflare Workers + D1.

Implementation:

  • License key generation and validation
  • JWT token for offline verification
  • D1 database for license storage

Trade-offs:

  • ✅ Robust security, centralized management, scalable
  • ⚠️ Internet required for initial activation
  • ⚠️ Requires separate admin UI development

Known Limitations & Notes

  1. Browser Support: Requires Web Bluetooth API support (Chrome, Edge, Opera)
  2. Camera: Requires HTTPS or localhost
  3. Print Size: Currently hardcoded to 30x50mm (Niimbot default)
  4. Image Count: Maximum 4 images per session
  5. PIN Default: Change default PIN (1234) in production
  6. Voucher: Disable in production if not used

API Reference (Internal)

Settings Schema

interface CustomizationSettings {
pageSettings: {
background: {
type: "Gradien" | "Warna" | "Gambar";
color?: string;
gradientStart?: string;
gradientEnd?: string;
};
title: { text: string; font: string; size: string; color: string };
subtitle: { text: string; font: string; size: string; color: string };
};
frameSettings: {
header: {
active: boolean;
text: string;
textColor: string;
fontSize: string;
fontFamily: string;
textAlign: string;
bold: boolean;
italic: boolean;
underline: boolean;
height: number;
};
footer: {
active: boolean;
text: string;
textColor: string;
fontSize: string;
fontFamily: string;
textAlign: string;
bold: boolean;
italic: boolean;
underline: boolean;
height: number;
};
background: { type: string; colorStart: string; colorEnd: string };
photoFrame: {
active: boolean;
thickness: number;
style: string;
color: string;
horizontalMargin: number;
};
};
button: ButtonSettings;
security: SecuritySettings;
printer: PrinterSettings;
voucher: VoucherSettings;
}

Quick Reference for New Engineers

Starting Development

  1. Clone repo → npm installnpm run dev
  2. Open http://localhost:3000
  3. Default PIN: 1234 → Access /customization

Key Files to Modify

TaskFile(s)
Add new framesrc/data/frames.json
Change defaultssrc/providers/SettingsProvider.tsx
Modify print logicsrc/providers/PrinterProvider.tsx
Image processingsrc/lib/image-processing.ts
Add pageCreate in src/pages/
Stylingsrc/app/globals.css, tailwind.config.ts

Debugging Tips

  1. Bluetooth issues: Check browser compatibility (Chrome required)
  2. Camera issues: Ensure HTTPS or localhost
  3. Storage issues: Check browser storage quota
  4. E2E tests: Use NEXT_PUBLIC_E2E=true for mock printing

End of Technical Documentation