Lovio started, literally, as the website for this wedding. It grew into a Swedish wedding-planning SaaS before the wedding it was built for had happened — which produced a deadline unlike any I've shipped against in twenty years. The date could not move. The users could not churn. And I would be standing in the middle of the production environment wearing a suit.
The shape of the product by July: a pnpm monorepo with five apps — the Next.js admin dashboard, a standalone no-login RSVP app, the SnapShare guest camera PWA, the vendor marketplace, and a React Native app for couples — plus a shared package and a Yjs relay. Firestore across three named databases, Cloud Run in europe-north1, every merge to main deploying straight to production. 685 commits since the monorepo came together in March. 322 of them landed in July. That ratio is what a wedding does to a roadmap.
The week before: building for a barn with no wifi
The venue is a converted round barn in the countryside, and the site visit established the constraint that ate the final week: effectively no wifi. Every day-of feature had been built the way you build web software — assume the network, degrade politely. Now the network was the thing degrading, at the one event where nothing was allowed to.
The presentation system took the brunt. The toastmaster runs slides on an iPad venue screen, controlled from a phone, and the speech videos were Google Drive links that buffered like it was 2009. The fix, landed the day before the wedding, was local-file video slides: a slide gains an optional localFileName, and at showtime the presenter points the display device at a folder of pre-downloaded files. Matching files play through the existing gesture-blessed video layer via blob URLs; anything unmatched falls back to the link. The folder picker alone needed three strategies — Chrome's directory API, webkitdirectory, and a multi-file fallback for the iPad that would actually be on stage.
That same week also surfaced an iPad-specific WKWebView compositing bug that rendered Drive video as audio-over-black, a Spotify integration that had to be migrated to their restricted February-2026 API before the toastmaster could use music pauses, and a slideshow that showed black frames during photo bursts. None of these were visible on my desk. All of them were visible on the actual iPad, on the actual venue's actual bandwidth. The last stretch of pre-wedding work was almost entirely this category: bugs that only exist on the hardware and network you'll really be standing in front of.
The bug that almost ate the personal messages
The near-disaster had nothing to do with wifi. We had written a personal message to every single guest — over a hundred of them, attached to the digital invitations, the detail people mentioned most. Nine days before the wedding I found that the mobile app's guest-detail sheet was silently erasing them.
The API's PUT handler for invitations treated a partial body as a full replace: it unconditionally wrote personalMessage, address and comments, coercing anything the caller omitted to an empty string. The mobile sheet saved only name and persons — and fired even on a zero-edit dismiss. So every time we opened a guest on the phone and closed the sheet, that guest's handwritten message, address and comment thread were wiped. The web dialog round-tripped all fields, which is why the bug stayed invisible for weeks: it only bit the client that sent less.
The fix was the boring, correct one — make the API honor partial updates so even old mobile builds are safe the moment the server deploys, echo the fields from the client as defense in depth, and lock the contract with a regression test. But the part I'm gladdest about is what got written alongside it: a recovery runbook and restore tooling, checked into the repo, that identified which invitations had been wiped and restored them. We got every message back. The lesson is old and it never stops being true: a PUT that fills in blanks for you is a data-loss bug wearing a convenience costume.
Dogfooding did not find that bug because I am a diligent tester. It found it because my wife noticed a message she had written was gone. There is no QA process more motivated than that one.
The day itself
What held: RSVP and seating had done their jobs weeks earlier — the budget module tracked us from a 250,000 kr plan to a 230,000 kr landing, and over a thousand SMS went out through the messaging system across the planning year. On the day, SnapShare took a hundred guests uploading from two bars of rural signal, the slideshow's new queue rail pulled incoming photos onto the screen during dinner, and the toastmaster ran the whole evening — slides, speech videos from local files, Spotify pauses — from their phone. The shoe-game questions were printed straight from the party-games tool without us knowing; being genuinely surprised by output from your own product is a strange and wonderful QA result.
What didn't hold, held later. Guests recorded videos in the photobooth that wouldn't play back — webm files with broken duration metadata, recorded at whatever resolution the phone felt like. Fixed the morning after: sane resolution caps, MP4-first, duration patched. The slideshow got a low-res-thumbnail-first pass the same day so photos stop going black on weak wifi. When your users are your wedding guests, the post-incident review happens at the brunch.
The 26% of photos that were upside down
The best engineering story of the whole month is also the most embarrassing one. Guests shot landscape photos in SnapShare, and a chunk of them came out upside down. In the two weeks before the wedding, six separate commits tried to fix landscape rotation by flipping the ±90° mapping back and forth, each one tuned on a single grip on a single phone. It was still wrong at the wedding.
Post-wedding forensics on the real photo set explained why no amount of flipping could ever have worked. Every sampled landscape capture arrived from the camera already 4032×3024 with no EXIF orientation — so the uprighting function's width-greater-than-height early return fired and no rotation was applied at all. The ±90 branches everyone kept re-tuning never even executed. And the actual defect — 26% of landscape photos inverted, clustered by which way the guest had turned the phone — needed a correction of exactly 180°, a value the function's codomain, {0, +90, −90}, could not express. Six commits guessed. None of them could have been right.
The fix that finally landed replaces the guessing with measurement: a debug overlay that prints one calibration line per capture — screen angle, camera facing, capture path, decoded shape — and a pure, total rotation function keyed on that tuple, with the device-specific corrections living in a small measured table. Filling in a cell is now a one-line change with a test instead of another global flip. It also brought the camera code its first real test coverage: 88 tests, including regression locks that portrait grips are never rotated and the two landscape grips stay exactly 180° apart.
Six commits tuned a branch that never ran. The seventh added instrumentation. Measure before you flip — I've known that for twenty years and still needed my own wedding photos to re-learn it.
The push notifications that only failed for guests
One more post-wedding find deserves its own section because it's such a clean Cloud Run lesson. Guestbook greetings and photo comments are supposed to push-notify the couple. Ours didn't — but only sometimes, in a pattern that made no sense until it did. The send path was fire-and-forget: return the response, then do the Firestore read and FCM sends in the background. Cloud Run with default CPU throttling freezes the instance's CPU the moment the response goes out, so those sends silently stalled. Admin-triggered pushes limped through because the app's own follow-up refetch woke the instance back up. Guest-triggered ones — greetings, comments, RSVPs — had no follow-up request, so they never went out at all. The failure pattern was literally 'works when we test it, fails when a guest does it.' The fix is one word long: await.
Who wrote all this
The same split I've written about all year, under more pressure than usual. The co-author trailers on the monorepo read like a model release history — Fable 5 on 190 commits, Opus 4.6 and 4.8 on nearly two hundred more, six Claude generations in total as the models moved forward mid-project. The security audit before the wedding ran as agent waves, with an adversarial review wave that caught six regressions in the fixes themselves. Agents wrote the rotation table, the recovery runbook, the folder-picker fallbacks. I supplied the constraint no model could have known: which Saturday, which barn, which hundred people, and which bugs were allowed to exist in front of them — none.
In February I wrote about Lovio's Saturday test: would I deploy this on a Friday at 4pm, knowing whose wedding is tomorrow? This July the question stopped being a thought experiment. I deployed on the Thursday. The wedding was mine. We danced until four in the morning, and about 500 favorite photos — the right way up, mostly — are in the album the guests are still adding to. The softer version of this story, in Swedish, is on lovio.se. This was the version with the stack traces.