A mobile‑first, QR‑authenticated photo booth for weddings. Guests scan a personal QR on their place card, take a selfie, pick an emoji, choose Team Bride or Team Groom, then snap photos and short videos throughout the night — every upload appears live on the projector slideshow. At the end, a single script stitches everything into a memory video.
| Layer | Technology |
|---|---|
| Front‑end | Next.js 16 (App Router), React 19, Tailwind CSS v4, Motion |
| Back‑end | Next.js Route Handlers, Server Components |
| Database | PocketBase 0.39.4 (SQLite + file storage, admin SDK) |
| Auth | HMAC‑signed cookies, per‑guest QR tokens (96‑bit entropy) |
| Real‑time | Server‑Sent Events (Next.js → browser) with live PocketBase subscribe |
| Reverse proxy | Caddy (auto‑TLS, Let’s Encrypt) |
| CI/CD | GitLab CI (7‑stage pipeline, semantic‑release, tag‑driven deploys) |
| Container | Docker Compose (3 services on an internal wedding
network) |
Internet ──▶ Caddy (:80/:443) ──▶ Next.js (:3000) ──▶ PocketBase (:8090)
│
▼
PocketBase (:8090)
bind‑mount pb_data
bind‑mount pb_migrations (ro)
The browser never talks to PocketBase directly — every read, write, media proxy, and real‑time connection goes through the Next.js server with superuser‑only API rules.
motion AnimatePresence, real‑time SSE from PocketBase,
author badge with name + emoji + team, shows only today’s photos (until
6 AM party cutoff).fluent‑ffmpeg script
stitches all photos/videos into final-wedding-memory.mp4
with crossfades./api/auth.# 1. Install dependencies
cd app && pnpm install
# 2. Start PocketBase locally
./pocketbase serve --dir=/home/mz/pb_data \
--migrationsDir=$PWD/pocketbase/pb_migrations
# 3. Create a superuser (on first run)
./pocketbase superuser create <email> <password> --dir=/home/mz/pb_data
# 4. Set env vars
cp .env.example .env.local # edit WEDDING_SESSION_SECRET, POCKETBASE_ADMIN_*
echo 'POCKETBASE_URL=http://127.0.0.1:8090' > .env.development.local
# 5. Generate QR guest cards
node scripts/generate-qr-codes.mjs names.csv
# 6. Start Next.js
./node_modules/.bin/next devOpen http://192.168.1.x:3000 on a phone on the same
Wi‑Fi (the dev server prints the LAN IP).
# One‑time VPS setup
docker network create wedding
mkdir -p /opt/wedding/pb_data /opt/wedding-dev/pb_data
# Build and run
docker compose -f infra/compose.yml build
docker compose -f infra/compose.yml up -dThree containers, all on the internal wedding network: -
caddy — TLS termination, ports 80/443. -
wedding-app — Next.js standalone build, port 3000
(internal only). - pocketbase — PocketBase 0.39.4, port
8090 (internal only).
A separate compose.develop.yml handles the
dev.am-party.pl staging environment on the same host.
scan ──▶ check ──▶ test ──▶ build ──▶ release (main) ──▶ deploy
│ │ │ │ │ │
SAST Biome vitest Docker semantic‑release SSH → VPS
Secret tsc registry GitLab Packages compose up
Deps tags
main additionally run
semantic‑release (conventional commits → version bump →
Git tag → GitLab Release → publish npm package).v1.2.3) trigger the
production build & deploy pipeline.develop auto‑deploys to
dev.am-party.pl.docs/index.md, README.md, and
pocketbase/schema.md)..
├── app/ # Next.js application
│ ├── app/ # App Router (pages, api routes)
│ ├── lib/ # Server utilities (auth, filters, media)
│ ├── pocketbase/
│ │ ├── pb_migrations/ # PocketBase schema (auto‑applied)
│ │ ├── types.ts # DB record types
│ │ └── schema.md # Human‑readable schema reference
│ ├── public/ # Static assets (bride.png, groom.png, …)
│ ├── scripts/ # One‑shot Node scripts
│ │ ├── generate-qr-codes.mjs # Guest QR generator
│ │ ├── generate-video.mjs # Night‑end video stitcher
│ │ └── seed-newlyweds.mjs # Bulk upload to Newlyweds collection
│ ├── Dockerfile # Multi‑stage standalone Next.js build
│ ├── next.config.ts
│ └── package.json
├── infra/ # Deployment infrastructure
│ ├── compose.yml # Production Docker Compose
│ ├── compose.develop.yml # Develop‑environment Compose
│ ├── Caddyfile # Reverse proxy + TLS config
│ └── pocketbase/
│ └── Dockerfile # PocketBase 0.39.4 container
├── docs/ # GitLab Pages source
├── .gitlab-ci.yml # Full CI/CD pipeline (7 stages)
├── .releaserc # semantic‑release configuration
└── README.md
| Variable | Purpose |
|---|---|
WEDDING_SESSION_SECRET |
64‑char random hex — HMAC signing key for auth cookies |
POCKETBASE_URL |
PocketBase base URL — http://pocketbase:8090 in
Docker |
POCKETBASE_ADMIN_EMAIL |
PB superuser email |
POCKETBASE_ADMIN_PASSWORD |
PB superuser password |
WEDDING_APP_URL |
Public base URL (for QR generation) |
WEDDING_TIMEZONE |
IANA timezone for day‑boundary calculations |
WEDDING_PARTY_CUTOFF |
Hour (0–23) — photos from the previous day still appear until this local hour |
This project follows Conventional Commits for automated semantic versioning:
feat: — new feature (minor bump)fix: — bug fix (patch bump)BREAKING CHANGE: in the body or !: after
the type (major bump)docs:, chore:, style:,
refactor:, perf:, test:,
ci:, build: — no version bump