Photobooth Web App - Technical Documentation
Version: 1.0.0
Last Updated: February 2026
Status: Production-ready
Table of Contents
- Executive Summary
- Technology Stack
- Architecture Overview
- Project Structure
- Application Flow
- Core Components
- State Management
- Services & Utilities
- PWA & Offline Support
- Configuration & Settings
- Testing
- Build & Deployment
- 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:
paymentsfor transaction records and provider statussubscriptionsfor recurring lifecycle stateentitlementsas the source of truth for access windowslicensingfor 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
- Pages Router: Uses Next.js Pages Router (not App Router) for simpler integration with Serwist service worker
- React Context: Lightweight state management without external libraries like Redux
- Client-Side Rendering: Most pages use client-side features (Bluetooth, camera, storage)
- 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
-
Home Page (
/)- Entry point with customizable start button
- Shows "Connect Printer" button if printer enabled
- Clears previous session verification
-
Frame Selection (
/frame-selection)- Displays available frame templates (1-4 photos)
- Shows voucher scan if voucher system enabled
- Passes frame ID to next page
-
Image Selection (
/image-selection)- Choose between camera or gallery upload
- Detects camera availability
-
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
-
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
-
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
-
Customization (
/customization)- PIN-protected settings page (default: 1234)
- Tabs: Page, Frame, Button, Printer, Voucher, Security
- Settings persisted to IndexedDB/localStorage
Core Components
Layout Components
| Component | Purpose |
|---|---|
SharedLayout | Common page wrapper with header, back button, content area |
PageTransition | Framer Motion page transitions |
UI Components (Radix-based)
| Component | Description |
|---|---|
Button | Customizable button with theme support |
Dialog | Modal dialogs |
Select | Dropdown selection |
Slider | Range input (brightness control) |
Switch | Toggle (on/off settings) |
Tabs | Tabbed interface for customization |
Feature Components
| Component | Purpose |
|---|---|
Frame | Frame preview with settings applied |
VideoPreview | Camera live preview + capture |
ImageThumbnails | Multi-image slot display |
VoucherScan | QR code scanner for voucher validation |
LockWidget | PIN entry overlay for customization access |
OfflineIndicator | Network status indicator |
InstallPrompt | PWA 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
useLocalStoragehook with IndexedDB + localStorage fallback - Supports settings migration for schema updates
- Default values defined in provider
Stored Settings:
pageSettings: Background, title, subtitleframeSettings: Header, footer, background, photo frame stylingbutton: Start button text, colors, border stylingsecurity: PIN code for admin accessprinter: Enable/disable, default copies, max copies, paper sizevoucher: 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:
- IndexedDB - Primary storage (PWA-reliable)
- localStorage - Fallback, synced with IndexedDB
Key Functions:
getItem(key)- Read from storagesetItem(key, value)- Write to storageremoveItem(key)- Remove a persisted valueclear()- Clear all persisted values
Image Processing
File: src/lib/image-processing.ts
Canvas-based image manipulation:
Functions:
generateCollage(images, settings, width, height)- Creates combined imageatkinson(imageData, threshold)- Applies Atkinson ditheringadjustBrightnessContrastGamma()- Print compensationcalculateCropCoordinates()- 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
| Hook | Purpose |
|---|---|
useCamera | Camera stream management |
useImageCapture | Photo capture to canvas |
useImageUpload | Gallery file handling |
useImageDimensions | Target dimensions calculation |
useFrameConfig | URL param parsing for frame |
useOffline | Offline detection |
useInstallPrompt | PWA 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 Type | Strategy | Cache Name |
|---|---|---|
| Static assets (images, fonts) | Cache-first | static-assets-v1 |
| API/JSON | Cache-first | api-cache-v1 |
| Navigation (pages) | Cache-first | pages-cache-v1 |
Offline Behavior:
- All app pages pre-cached
- Falls back to
/offline.htmlfor 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 photo2→ 2 photos3→ 3 photos4→ 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
| Variable | Purpose | Default |
|---|---|---|
NODE_ENV | Environment | development |
NEXT_PUBLIC_E2E | E2E test mode | false |
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
- Browser Support: Requires Web Bluetooth API support (Chrome, Edge, Opera)
- Camera: Requires HTTPS or localhost
- Print Size: Currently hardcoded to 30x50mm (Niimbot default)
- Image Count: Maximum 4 images per session
- PIN Default: Change default PIN (1234) in production
- 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
- Clone repo →
npm install→npm run dev - Open
http://localhost:3000 - Default PIN:
1234→ Access/customization
Key Files to Modify
| Task | File(s) |
|---|---|
| Add new frame | src/data/frames.json |
| Change defaults | src/providers/SettingsProvider.tsx |
| Modify print logic | src/providers/PrinterProvider.tsx |
| Image processing | src/lib/image-processing.ts |
| Add page | Create in src/pages/ |
| Styling | src/app/globals.css, tailwind.config.ts |
Debugging Tips
- Bluetooth issues: Check browser compatibility (Chrome required)
- Camera issues: Ensure HTTPS or localhost
- Storage issues: Check browser storage quota
- E2E tests: Use
NEXT_PUBLIC_E2E=truefor mock printing
End of Technical Documentation