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.

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

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

“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:
“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.




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.