Skip to content
← All projects

Case study · built for Hazel & Jhonel

Wedding RSVP Platform — a premium guest experience with a real backend

A production application in two halves: a luxury single-page invitation that guests reply to in under a minute, and a private, Supabase-authenticated dashboard where the couple runs their guest list. Designed, built, and deployed end to end for a real wedding — this page explains how, using only what the repository itself can verify.

Role
Design & engineering, end to end
Stack
Next.js 16 · TypeScript · Tailwind CSS 4 · Supabase · Nodemailer
Client
Hazel Jean & Jhonel Rhey — November 2026
Status
Live on Vercel — taking real RSVPs
routes
14
Postgres tables
8
SQL migrations
4
runtime dependencies
6
tagged releases
4
real wedding
1

01

Overview

This is two products sharing one system: a guest experience (invitation, story, details, schedule, gallery, FAQ, and a two-step reply card) and a couple’s dashboard (live statistics, a guest list with a status workflow, CSV export, content editors, and email controls). Guests never create accounts; the couple signs in through real authentication. The boundary between the two halves is enforced in the database, not just the UI (§06).

Everything below is verifiable: the product is live, the repository is public, and the comments quoted here can be read in full.

The invitation hero — the couple's names, date, venue, and a live countdown over a full-bleed photograph
The production invitation — names, date, venue, RSVP call-to-action, and a live countdown to the ceremony, over the couple's own photograph.

02

The challenge

A real wedding is a project with a hard deadline. RSVPs collected over messages and paper cards scatter; guest counts, meal choices, and dietary notes end up in someone’s notebook; and every “are we confirmed?” question costs the couple an evening. The brief, in product terms:

Zero-friction for guests

Titas and grandparents reply on their phones. No accounts, no app, no PDF — a reply card that takes under a minute and confirms instantly.

One source of truth for the couple

Every response, guest count, meal choice, and dietary note in one place — reviewable, searchable, and exportable for the caterer and seating chart.

An admin that reads like a planner

The couple isn't technical. Editing the schedule, gallery, or FAQ must feel like filling in a wedding planner, not administering a database.

Luxury, not template

The invitation had to carry the wedding's own black-and-gold identity — editorial typography, a countdown, music — and still load fast on a phone signal.

03

The solution

One Next.js application with a hard privacy line down the middle. The public half is a statically-regenerated, single-page invitation; the private half is a session-authenticated dashboard. Supabase Postgres is the single source of truth on both sides, and row-level security decides — in the database — what each side may do:

Guests (anonymous)

read the invitation · submit one RSVP · download the .ics invite

The couple (authenticated)

review & confirm RSVPs · edit every content section · send email

Supabase Postgres — row-level security

anon: SELECT content, INSERT rsvps — nothing else · authenticated: manage content, read & update rsvps (no delete)

Email — Nodemailer over SMTP

new-RSVP notification to the couple · confirmation to the guest, sent exactly once and tracked per row

The privacy boundary lives in Postgres policies, so even a bug in a route or action cannot widen what a request may touch.

04

Guest experience

The guest journey is one page, top to bottom: hero and countdown, the couple’s story, ceremony and reception details, the day’s schedule, a gallery with a lightbox, FAQs, and finally the reply card — with a background music player that waits politely for a gesture and remembers being turned off. Every entrance animation respects prefers-reduced-motion.

The RSVP itself is a two-step reply card: accept or decline first, then a form whose fields, labels, required flags, meal options, and guest-count limit all render from the couple’s stored configuration (§05). Validation runs server-side; a duplicate email meets a friendly “already on our list” panel backed by a unique index on lower(email); a successful reply gets a personalized thank-you with a drawn checkmark and a calendar download.

The RSVP reply card — Joyfully Accepts or Regretfully Declines, on the dark ink band
Step one of the reply card — accept or decline, in the wedding's black-and-gold. Declining collapses the form to identity and a message; accepting reveals guest count, meal, and dietary fields.
features/rsvp/actions.ts — verbatim from the repository
“Scheduled after the response so the guest never waits on SMTP, and the service swallows failures so the saved RSVP is never rolled back.”

What this proves: the guest path is treated as sacred. Email — the least reliable dependency in the system — is scheduled with Next’s after() so a slow or failing SMTP server can never lose an RSVP or delay the thank-you on screen.

05

Admin experience

Behind /login sits the couple’s dashboard — ten authenticated pages that run the wedding. Sign-in is Supabase Auth with two doors: password, or a magic link(sent only to existing accounts, so the form can’t be used to probe for emails). There is deliberately no self-serve sign-up; the couple’s accounts are provisioned directly.

The overview greets them with live numbers — responses, accepted, declined, seats coming, pending review. The RSVP table is searchable and filterable with a detail drawer, a pending → confirmed / contacted status workflow, and one-click CSV export of the filtered list for the caterer. Every content section of the invitation — details, story, schedule, gallery, FAQs, and the RSVP form itself — is editable in place, with drag reordering where order matters.

The couple-dashboard sign-in — password and email-link tabs on an ivory card
The dashboard door — Supabase Auth with password and magic-link sign-in. The private half of the product lives behind it: overview, RSVPs, six content editors, and settings.
features/dashboard/rsvps/actions.ts — verbatim from the repository
“Confirming an accepted RSVP automatically sends the confirmation email exactly once: if a previous send succeeded, it is NEVER auto-resent (only the dedicated resend action may). An email failure never rolls back the status change.”

What this proves: the workflow thinks about failure the way operations people do. The once-only rule reads tracking state beforethe update so it cannot race; the outcome of every send — sent, failed, message id, error — is recorded on the RSVP row itself; and a “Send test email” button in settings lets the couple verify delivery end to end before the invitations go out.

06

Technical architecture

The production dependency list is six packages: next, react, react-dom, @supabase/supabase-js, @supabase/ssr, and nodemailer — no ORM, no component library, no form library. Reads and writes go through Server Actions; the public page regenerates on a one-hour ISR window so content edits appear without a deploy.

Access control is layered three deep. The route proxy (Next 16’s middleware successor) redirects anonymous visitors away from /dashboard/** using a JWT-verified getUser(); the dashboard layout re-checks the session on render; and even if both were bypassed, the RLS policies below are what the database will actually permit. The schema is multi-wedding by design — every table hangs off weddings.id — and the migration is honest about what that does and doesn’t yet mean:

supabase/migrations/00002_dashboard.sql — verbatim from the repository
“The couple signs in via Supabase Auth; authenticated users manage content. NOTE for multi-wedding future: these grant every authenticated user access to every wedding. Before onboarding a second couple, add an ownership mapping (e.g. wedding_members(wedding_id, user_id)) and scope these policies to it.”

The schema — 8 tables, 4 migrations

  • weddings — identity, palette, form settings, music
  • rsvps — responses + email tracking columns
  • story_milestones · schedule_items
  • gallery_images · faqs
  • meal_options · rsvp_form_fields

A unique index on (wedding_id, lower(email)) makes duplicate replies a database guarantee, not a UI hope.

Resilience decisions

  • Seed fallback: with no Supabase configured the presentation layer renders from bundled seed content — but RSVP writes require the live database and fail loudly, never silently.
  • Config-driven form: the reply card renders from rsvp_form_fields + meal_options, with sensible defaults if migrations lag.
  • Friendly failure: Postgres errors map to human messages; SMTP errors are stripped of secrets before they ever reach a screen.

07

The product, in pictures

The journey from invitation to reply, as guests actually see it — captured from the live deployment.

Wedding details — ceremony and reception cards with times, venue, directions, dress code, and palette
The Where & The When — ceremony and reception cards with directions, alongside dress code, palette, and parking. Every word here is editable from the dashboard.
The wedding-day schedule — a timeline of the day's moments
The day's schedule — a drag-reorderable timeline in the dashboard, a quiet editorial list for guests.
The gallery — a grid of the couple's photographs with a lightbox
The gallery — a lightboxed grid the couple curates themselves.
The invitation on a phone — names, date, and RSVP call-to-action stacked for mobile
The mobile invitation — where most RSVPs happen. Mobile-first Tailwind throughout; no separate layouts.

08

Results

No invented metrics here — the honest results are operational, and they are the point:

  • The platform is live in production, collecting real RSVPs for a real wedding ahead of the November 2026 date — deployed on Vercel with Supabase as the system of record.
  • Guests reply without accounts, on their phones, and get an instant personalized confirmation plus a calendar file; duplicates are impossible by database constraint.
  • The couple runs the entire wedding — guest list, statuses, every content section, email — without a developer in the loop. The caterer’s list is one CSV export away.
  • Operationally quiet by design: email failures cannot lose data, error messages never leak secrets, and content edits go live within the hour without a deploy.

09

Lessons learned

What this build taught — and, in the same evidence spirit, what it deliberately does not do yet:

Put the boundary in the database

Route guards are UX; RLS is security. Writing the policies first made every later feature safe by default — a bug in an action can't widen what Postgres permits.

Never let email touch the critical path

Deferring sends with after(), swallowing failures, and tracking outcomes per row means the flakiest dependency in the system can't break the most important moment in it.

Configuration beats hardcoding — even for one client

The form fields, meal options, and limits live in the database. The payoff arrived immediately: the couple tunes their own form, and wedding number two is a data problem, not a rewrite.

Document the seams you're not crossing yet

Multi-wedding is schema-ready but the RLS scoping is honestly deferred — the migration says exactly what to add (wedding_members) before a second couple onboards. Known limits today: no per-wedding roles, image URLs rather than uploads, and no guest self-editing.

Beside KATHA, this project closes a loop: KATHA proves the product craft with a deliberately deferred backend; this platform proves the backend — real authentication, real Postgres with row-level security, real email — shipped to production for people who are counting on it.

Read the code, then let's talk

The repository is public and every claim on this page is checkable against it.