Tell your crush without saying a word
HeyBeautiful started with a personal problem: the gap between noticing someone and saying something real is broken. Phone numbers are cold, dating apps reduce people to thumbnails, and pickup lines die in ten seconds. The answer was a scroll-driven, animated micro-site that anyone can build for their crush — personalised photos, word-by-word text reveals, puzzles, trait cards — and share as a link. No app download for the viewer, no pressure, no performance required.
The creator builds a page in a visual drag-and-drop editor with 30+ section types: text letters, Polaroid galleries, sealed envelopes, iMessage-style chat replays, origami reveals, scratch-cards, Spotify embeds, ambient audio, and more. They set a unique slug, pick from 8 emotional colour palettes, and publish. When the crush opens the link they experience a story that unfolds as they scroll: text fills colour word-by-word, photos develop Polaroid-style, background blobs breathe through the mood palette. At the end they choose whether to reveal the creator's contact — WhatsApp one-tap, vCard, or QR code — entirely on their own terms.
The core emotional differentiator is real-time presence: the creator sees when the crush opens the page, which section they are reading at any moment, and receives Web Push notifications when a puzzle is solved or contact is revealed. The platform shipped live at heybeautiful.one and was architected from day one as white-label ready for ThemeForest.
The project had a hard constraint: 41 days, solo, alongside two other active freelance projects. The product required genuinely complex frontend engineering — scroll-driven animation across 30+ section types, a polymorphic drag-and-drop editor, a real-time presence layer — on top of a complete SaaS backend: payments, magic-link auth, 13 AI writing endpoints, 4-language i18n, PWA with offline support, custom domain middleware, and a content moderation admin panel. Every architectural decision had to optimise for shipping speed without creating debt that would block the ThemeForest path.
The presence system was the most critical architectural question. Supabase Realtime was the obvious choice but would lock the project to a cloud vendor and struggle under high-frequency scroll events. WebSockets are awkward in Next.js App Router's serverless model. The product's core emotional promise — "see when she opens your page" — could not be a known gap at launch.
The stack is a Turborepo monorepo with Bun workspaces. Next.js 16 App Router, React 19, Tailwind CSS v4. GSAP 3 with ScrollTrigger drives scroll-based animations; Framer Motion handles UI transitions; Lenis provides smooth scroll. Drizzle ORM manages a 30-table PostgreSQL 17 schema across 36 API route groups.
Page section content is stored as a JSONB column on crush_pages rather than relational rows.
Sections are always read and written together; a relational table would require either a column per
section type or a wide EAV model. Drizzle's jsonb() type keeps TypeScript inference clean at the
cost of no SQL-level filtering inside section content — acceptable at this scale.
Real-time presence runs on Dragonfly pub/sub via ioredis. The viewer-side usePresenceBroadcast
hook broadcasts scroll depth and current section index, throttled to a delta greater than 5% or a
section change — reducing event volume by roughly 20x while keeping creator visibility accurate.
Stripe handles four subscription tiers; the singleton is marked server-only to guarantee the
secret key can never appear in a client bundle.
JSONB sections over relational rows. Sections are polymorphic — a text block, a Polaroid gallery, and a Spotify embed share a table row but have entirely different shapes. JSONB stores each as a typed object without requiring schema migrations for every new section type added during the sprint.
Dragonfly over Supabase Realtime or WebSockets. Self-hostable, faster for high-frequency scroll events, and the pub/sub SSE pattern is idiomatic with the App Router's serverless model. Locking to a cloud vendor for the product's most important feature was the wrong trade.
White-label config package from day one. siteConfig, themeConfig, and emotionalColors live
in a shared packages/config workspace. A ThemeForest buyer can rebrand the entire product by
editing one file. This cost roughly 10% extra upfront and saved all component surgery at handoff.
Graceful degradation on every optional service. Stripe, Anthropic API, VAPID push, and MinIO are all behind runtime guards. Any one missing returns a clean 503 or silently hides the feature in UI. The app runs with just PostgreSQL and Dragonfly — the self-hosted happy path.
Emotional arc through animation timing. A tempo map in ANIMATIONS.md defines GSAP duration
by section type: slow (2–3s) for introduction, very slow (3–5s) for the intimacy section. Pacing is
a first-class design constraint enforced at the component level, not left to intuition.
roadmap items shipped
Claude AI endpoints
solo TypeScript
Production-grade developer portfolio and full admin CMS with AI, 3D scenes, and real-time features
Animated WebGL launch-announcement page with GLSL shaders, Supabase waitlist, and SMTP email confirmation
Did this resonate?
HeyBeautiful shipped live at heybeautiful.one with all 207 planned roadmap items completed across 35 feature phases — 44,000+ lines of TypeScript, 418 source files, 30+ animated section types, 36 API route groups, 13 Claude-powered AI writing endpoints, 4 Stripe tiers, 4-language i18n with RTL, a full PWA with offline service worker, and Playwright E2E tests. The white-label config package and Docker Compose self-host path position it for ThemeForest submission. The real-time presence system — section-level granularity, Web Push at four milestones — is the feature that separates it from every static link-sharing tool on the market, shipped as part of a 41-day solo sprint alongside two other active freelance projects.