← All projects

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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).

  8. 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.