Learn more
Umify
Guests scan a QR code, their photos appear live on the big screen — no app, no account.
- Status
- Live
- Timeline
- Since late August 2026
- Role
- Solo — product, frontend, backend, data, payments, emails, GDPR, SEO, DevOps and pricing
Context and problem
At a wedding, a birthday or a corporate party, most photos never leave the guests' phones. A flooded WhatsApp group, an app half the room will not install, and nothing to watch together during the night. The host wakes up with a handful of pictures and no shared moment on the screen.
Solution
Umify is a multi-tenant SaaS: the organizer creates an event, picks a plan, and displays a QR code. Guests scan it, send photos with no app and no account, and the projected wall updates in real time. After the event, the gallery can be downloaded if the organizer opens it, then files are purged according to the plan. The product was designed, built and shipped to production by one person.
Key features
Guest side
Photo upload from the phone, emoji reactions, photo challenges with a leaderboard, live polls and quizzes, evening schedule, countdown, timeline, private messages to the organizer.
Wall and big screen
Live photo wall in real time, spotlight, automatic retrospective with a music playlist, raffle, projected timer, full-screen announcements.
Organizer space
Creation assistant, interactive onboarding tailored to the plan (7 to 9 steps), photo moderation, emergency button, full settings.
Personalization
Six event types (Birthday, Wedding, Birth, Party, Corporate, Other) apply a palette (six in total), an animated mood (confetti, petals, balloons, stars, discreet), plus adapted copy, emojis and challenges. The organizer can then change everything.
After the event
Photo download, email recap, retention by plan, automatic purge.
Back-office /internal
Account search, bans, plan override outside Stripe, audit log, email resend with real data, GDPR erasure (art. 17).
Landing
Animated hero (canvas/3D) with a live photo wall, big-screen section, pricing, full SEO (sitemap, robots, JSON-LD, dynamic Open Graph, PWA manifest).
Architecture and technical choices
A deliberately tight stack around Next.js and Supabase, to ship a realtime, paid and compliant product without multiplying services.
Frontend
Next.js 16 (App Router, Route Handlers), React 19, TypeScript 5, Tailwind CSS 4, lucide-react.
Backend and data
Supabase (Postgres, Auth, Storage, Realtime, RLS) and Drizzle ORM, with 17 SQL migrations. Multi-tenant isolation lives in RLS policies, not in application-level filters.
Payments
Stripe: tiered checkout, upgrades, idempotent webhook. Prices are read from the database, never from the form.
Emails
Resend, 15 templates (11 product + 4 Supabase Auth) and a sending queue.
Quality and observability
Vitest (59 test files, including API-route integration tests), strict TypeScript, ESLint, Prettier, Sentry.
Infra
Docker and docker-compose on a VPS, with cron sidecars (Alpine + crond): email queue every 5 minutes, daily job at 06:00 UTC.
Other
Web Push, sharp (image processing), qrcode, pdfkit, jszip (ZIP export of photos).
Technical challenges
- 01
Multi-tenant security
Per-event data isolation through Supabase RLS. A public Realtime channel for guests and a private channel reserved for the organizer (RLS policy on realtime.messages), so private messages never leak to guests.
- 02
Server-side plan gating
Every paid feature is enforced in the API (403 citing the minimum required plan). The server never trusts the client. An integration test suite on the routes locks the rules per plan.
- 03
A single source of truth for pricing
Prices, features, moods, retention windows, badges and the onboarding guide are derived from config/pricing.ts. A plan change propagates everywhere with no other code edits.
- 04
Reliable scheduled jobs
Two sidecars (email queue every 5 minutes, daily job at 06:00 UTC) and six daily jobs deduplicated via sentAt columns. Retention per plan is recomputed on every run, including after an upgrade.
- 05
Payments
Idempotent Stripe webhook, price read from the database never from the form, upgrades only to a higher tier, fallback resync of the payment status.
- 06
GDPR
Account erasure in a single call thanks to Postgres cascades, best-effort Storage cleanup, legal retention of invoices and the audit log. Terms and privacy policy aligned with the actual retention windows.
- 07
Rendering and accessibility
Deterministic animations (no Math.random, to avoid hydration mismatches), prefers-reduced-motion support on the landing, touch targets at least 40 px, accessible guide (named region, focus management).
- 08
Per-guest rate limiting
Limits are applied per guest, not per IP: a venue's wifi is shared. Covers uploads, reactions, votes and messages.
Numbers
Figures measured in the repository, not usage metrics.
~61,000
TS/TSX lines
~490
files
80
API routes
59
test files
17
SQL migrations
~150
commits in 4 weeks
Three plans, one payment per event
Free
€0
15 guests · 3 days retention
Essentiel
€69
Unlimited guests · 30 days
Complet
€139
90 days · all animations
What I learned
- Isolate multi-tenancy in Postgres (RLS) rather than in application code: the public Realtime channel and the organizer channel do not cross, even if the client is compromised.
- A single source of truth for pricing avoids duplicating rules across the landing, Stripe webhooks and the onboarding guide — a tier change propagates without hunting for hardcoded values.
- Gating every paid feature in the API, with per-route integration tests, is the only viable way to hold a freemium model: the client is not a source of truth.
- Rate limiting per guest, not per IP, is mandatory as soon as everyone shares the venue wifi.