Designer + engineer · April 2026

A wedding site for Mikey & Tania

Shipped

A custom wedding website and admin dashboard I designed and built for two close friends ahead of their May 2026 wedding. Live at mikeyandtania.com.

The landing page — the couple's names in Playfair, the hand-drawn basilica, the date plate the rest of the site hangs off. Enlarge it to flip between the light and dark themes.

How it started

Mikey and Tania are two of my closest friends. In early February 2026, three months out from their wedding, the conversation turned to whether they actually needed a wedding website. The off-the-shelf options — Squarespace, Withjoy, the usual list — were technically fine, but every one of them came with someone else's design template on the front and someone else's paywall behind it.

I offered to build them one instead. They said yes. That was the brief.

What I built

Two products in one repo, really. A guest-facing site that holds the where, when, schedule, dress code, and the RSVP form. And an admin dashboard, sitting behind it, where Mikey and Tania could see who'd responded, who they still needed to chase, the dietary requirements list for the caterers, and edit every page on the site themselves whenever something needed updating.

Guest · The Wedding
Admin · Pages
Two products in one repo: a guest page on the left, and the admin Pages list it's composed from on the right.

The site itself

The visual register is editorial — print magazine more than wedding-site-template. Three typefaces carry it: Playfair Display for the display headlines (the couple's names sit at 118px on desktop, with an italicised & in sage-dark linking them), Bricolage Grotesque for body and forms, and JetBrains Mono for the eyebrow micro-labels — the §01 · Radna · Romania kind of thing. It's the serif-display-plus-humanist-sans-plus-monospace-kicker combination that Vogue and The Atlantic use to signal "we're a magazine, not a brand asset." It's also what Mikey and Tania responded to most strongly when I was sketching directions in our early meetings — restrained, slightly serious, the opposite of bridal-template.

The colour system is monochromatic sage. Three shades of green — mid (#8e9977), dark (#6b7a5a), deep (#4a5a3a) — over a warm paper cream (#f1f2e9) that's never pure white, against an ink (#2a3024) that's never pure black. The only off-palette colour is a terracotta #8a4a3a, used exclusively for destructive actions in the admin. The whole palette comes directly from Mikey and Tania's wedding colours — they'd settled on sage for the day itself, and I built the rest of the system around what they were going to wear and what the venue would look like.

The hero is one screen, two columns. Left: a small location kicker, the couple's names, and two CTAs — primary RSVP, ghost View Details. Right: a hand-drawn SVG of the basilica in Radna where the ceremony took place, a vertical §01 · Radna, Arad County · Romania section marker borrowed from print, and a date plate with the day numeral set 92px in italic Playfair. Two radial sage gradients sit in the background corners, very subtle, doing the work of warming the page without leaning on a photo.

The same hero in the dark register — not an inverted light theme, but its own set of greens, designed on its own terms.

The basilica illustration is the standout craft moment in the whole project. Hand-drawn for Mikey and Tania specifically — not stock, not procedural — and the kind of thing you can only ship when the client is also a friend who'll let you take an extra afternoon on a detail nobody else asked for.

I made the call early to not use photography anywhere on the site. The basilica illustration and the typographic system carry the entire visual identity on their own, and once I'd committed to the editorial register the photos would have pulled the page back toward wedding-album-on-a-website territory. The result is a site that reads as designed rather than as documented, which is the distinction I cared about.

Mobile is the register where wedding sites usually fall apart, because most guests will check the site from a phone at the wedding itself, on a flaky reception signal, after one too many glasses of something. The hero ladders down across four breakpoints (118 → 88 → 68 → 56), the basilica's date plate loses its absolute-positioned anchor and the right column stacks vertically, the admin tabs move from inline to a second row, and any button label that wasn't strictly needed hides behind its icon. The site was designed mobile-first from the start — the desktop layout is the progressive enhancement, not the other way around.

The Travel page. Numbered sections, monospaced kickers, the same restrained system carried through every page.

The RSVP flow

A guest hits the site (through the password gate first — there's a shared wedding password that lets the URL go semi-public without being fully open), navigates to /rsvp, fills the form. Party size, names per attendee, dietary requirements, transport mode (own car vs the coach we'd hired for the venue), language preference. There's no separate invite link per guest and no individual guest accounts — the RSVP form is the way you enter the system at all. That trade-off keeps the data model tiny, at the cost of not being able to differentiate "didn't respond" from "wasn't invited."

The form writes to Convex via a mutation, fires a "New wedding RSVP" notification email via Resend to the configured admin address, and updates the dashboard in real time so Mikey and Tania see the response as it lands.

The RSVP form mid-fill. It writes to Convex through a mutation and updates the dashboard the moment a guest hits send.

The admin dashboard

The dashboard is where the actual work of running an event happens. It's built with the same design vocabulary as the guest site — same sage palette, same typeface system, same paper-tone surfaces — so toggling between them never felt like crossing into a different product.

When an admin signs in, they land on the Dashboard tab. A welcome strip naming the wedding date, three stats tiles (Days to go, RSVPs received with the declined-versus-accepted breakdown, Guests attending with kid count and dietary count), a meta strip pulling out the RSVP deadline and the last-edit timestamp. Below that, the RSVPs table itself — sortable by column, with the dietary requirements and transport mode visible inline, and a CSV export that's BOM-prefixed so Excel reads UTF-8 properly for the caterer's headcount sheet.

Editing an RSVP triggers an editorial drawer that slides in from the right over 260ms on a cubic-bezier curve, with the page behind dimming into a paper-tone backdrop. Name list, dietary notes, transport mode, language — all editable in place. Deleting one fires a confirm dialog that scales in over 220ms, paper-tone with a terracotta confirm button. That's the only place the off-palette accent lands in the entire product.

The dashboard: days-to-go, the RSVP count with its accepted-versus-declined split, and the sortable guest table with dietary needs and transport inline.

The thing that's easy to miss in a wedding admin is that this is also a small CMS. The Pages tab manages every guest-facing page as an ordered list of blocks — schedule blocks, list blocks, card blocks, group blocks, swatch blocks — each one editable per language (the wedding is bilingual, English and Romanian, served from the same Convex content row). Blocks drag-reorder via @dnd-kit, content edits through @tiptap/react, save into Convex.

Admin · add a block
Admin · block editor
The CMS underneath. Every guest page is an ordered list of blocks — pick a type from the picker, fill it in, drag to reorder.

The blocks also stand in for what most wedding sites surface as separate structured products. Gifts, accommodation notes, vendor recommendations — none of those are features in the codebase. They're card or text blocks with the right content inside them. The blocks-are-the-registry approach is the more interesting design call than building a structured gift-tracking UI would have been, and it lets Mikey and Tania shape each page to fit what they want to say rather than fight a form that already assumed the shape for them.

Building a real CMS for a one-off wedding site looks like overkill on paper. It would be, if I trusted myself to be available on the phone every time something needed changing in the two weeks before the day. I don't. The CMS was insurance.

The stack, briefly

Next.js + React + TypeScript — the safe defaults. Server components for the public site (fast first paint for guests on the train, on a phone, on a flaky connection), client components for the dashboard's interactive bits.

Tailwind + shadcn/ui — the design system foundation. shadcn rather than a full UI library because I wanted the components in the codebase, not behind a dependency boundary I couldn't tune.

Convex for the database, paired with better-auth for the identity layer. Convex stores everything I'd usually have a Postgres schema and a migrations folder for — RSVPs, page content, the admin user table, the invite tokens — and the queries auto-update in real time, so the dashboard reflects new responses as they land without me writing a subscription layer by hand. better-auth handles the bit Convex doesn't try to be: the Google OAuth flow, the JWE-encoded session cookie, the sign-in API routes. The two meet at the email — the client passes the signed-in user's email into a Convex mutation, and a small requireAdmin helper checks it against the adminUsers table before letting the write through.

The Convex trade-off is vendor lock-in, which for a wedding site is genuinely fine. For a paying business I'd think harder — more on that below.

Resend for transactional email. RSVP confirmations to the guest who responded, save-the-date reminders ahead of the cutoff, and the dashboard notifications to Mikey and Tania when something needed their attention.

What I'd do differently

The requireAdmin pattern, where the client passes the signed-in user's email to a Convex mutation and the mutation trusts that email to look up admin permissions, is the kind of shortcut that works because of social context. The admin table has two rows. The people on it are friends of mine. Nothing about this is going to be probed by a hostile actor before the wedding happens.

For a SaaS version of the same product, that pattern doesn't survive contact with reality. A malicious caller hitting Convex directly could pass any admin's email and bypass the check, as long as that email is in the table. It's the kind of thing you'd want to fix before letting strangers in.

If I were to spin this up as an actual product, the first thing I'd do is migrate the identity layer to Clerk. Their Organizations primitive maps almost embarrassingly cleanly to weddings-as-tenants. One org per wedding, invites and role-based access (owner / editor / viewer) built in, Stripe billing hooks already wired up. Convex has an official Clerk integration that exposes the verified user identity to mutations via ctx.auth.getUserIdentity(), which kills the trust-the-client-email pattern entirely. The user is who Clerk says they are, not who the client claims. That alone would be months of work inherited for free, against the alternative of extending better-auth to handle multi-tenant invites, org-scoped sessions, and tenant isolation by hand.

That's the only architectural call I'd genuinely change. The rest of the stack — Next.js, Tailwind, shadcn/ui, Convex for the data layer, Resend for the email — I'd reach for again tomorrow.

Beyond the architecture, there's a layer of smaller debt I'd clear before showing the codebase to anyone: the requireAdmin helper is repeated at the top of every Convex file rather than abstracted into a single wrapper; the qrcode dependency is still installed from a guest-check-in feature I never built; the legacy sections table is still in the schema as a backwards-compat shim from before the pages-and-blocks rewrite; and page content is stored as stringified JSON in a Convex field, which is pragmatic but means I can't query inside it ("find every wedding using Playfair as its display face" would need a structured columns refactor). None of those affect Mikey and Tania's wedding. They'd all affect a SaaS the moment a second tenant existed.

Closing

Building this for friends, against a deadline that wouldn't move, taught me two things I didn't quite expect. The first is that working for people I love made me harder on myself than I've ever been for any paying client — there's no spec to hide behind, no scope-creep argument to defer the bit that feels close-but-not-right. The second is that an immovable deadline turned out to be the most useful planning constraint I've had on a project. Nothing focuses an estimate quite like knowing the date comes whether you're ready or not.