Opuluxe Moments

- Ref
- P-01
- Period
- 2026 – now
- Status
- Live
A freelance build for a luxury event styling business, now their live site. It takes a visitor from browsing to a paid booking: an enquiry builder that turns interest into a complete brief, a backdrop designer that prices a design as it's built, and an admin portal that lets the owner run quotes, payments, content and emails without me.
The problem
Bespoke styling has no price list, so every enquiry arrived as an open-ended Instagram message: no date, no guest count, no budget. Each one took a long back-and-forth to pin down, and none of it was recorded anywhere. The site had to do that qualifying up front without feeling like a checkout, and the owner had to change prices and seasonal ranges without a developer.
What I built
Around seventy-five routes across three surfaces: a public marketing site, a set of customer tools, and an admin portal. Two tools sit at the centre. A thirteen-step 'Build Your Event' wizard gathers the occasion, guest count, venue, palette, budget, inspiration photos, backdrop and styling choices, delivery details and timings, and the business replies with a mockup. A canvas backdrop designer lets customers place backdrops, drapes, balloons, decorations and text in portrait or landscape, with undo and redo and a running estimate; a finished design is exported with the logo watermarked and joins their enquiry, and signed-in customers can save one and come back to it. Setups, offers, hire items and the Sunnah Table go into the same enquiry basket, and a postcode calculator estimates delivery. Customers get an account to track enquiries, update their details, change their password, withdraw an enquiry and pay, and every enquiry also has a payment link, so a guest can pay without one. The owner gets the enquiry workflow (quote, set a deposit, accept, decline) and edits nearly everything else from the admin portal: the catalogue, designer pieces and prices, event choices, hire pages, FAQs, offers, testimonials, policies, terms and the emails customers receive.
Architecture
Next.js App Router, one deployable. The marketing pages are statically rendered and read the catalogue straight from Postgres, so an admin edit revalidates the pages it affects instead of waiting on a deploy. Row Level Security is the authorisation boundary for everything the browser can reach, rather than the route handlers. The wizard holds its selection on the client and autosaves a draft locally. Submitting goes to a server action that checks the visitor is human with Cloudflare Turnstile, writes the enquiry with the service role, and only then emails the owner. Payments go through the enquiry's payment link to Stripe Checkout, confirmed by a signature-verified webhook rather than the browser redirect. Customer emails go out through Resend from templates the owner edits. Photos and designs live in object storage, and a nightly job deletes uploads that never became an enquiry. 52 test files run before every build and in CI.
Stack
Decisions
- 01Took the prices out of Build Your Event. The wizard now gathers a brief, a budget and inspiration, and the business replies with a mockup. Pricing moved to the backdrop designer, which estimates a design as it is built and won't finalise one under £200; the binding number is still a human decision.
- 02Made Row Level Security the boundary, not the route guards. The admin flag lives in a profiles table, never in auth metadata, because metadata is user-editable and surfaces in the JWT: checking it would be self-serve privilege escalation. The per-route requireAdmin() checks are defence in depth.
- 03Moved the catalogue and then nearly all site content into the database behind the admin portal (event choices, hire pages, FAQs, policies, terms and the customer emails), so none of it needs a deploy. The cost is that every admin write must revalidate the pages it touches, or an edit saves correctly and changes nothing visible.
- 04Write the enquiry to the database before sending the notification email. The provider had gone down before, and the portal reads from Postgres, so the row is the record and the email is only the alert. If the email fails after the row saved, the customer is still told it worked: telling them otherwise just makes them submit twice.
- 05Moved enquiry writes to the server. The publishable key ships to every visitor, so anyone could script inserts straight at the Data API without touching the form or its bot check. Now nothing in the browser can insert: the server checks Turnstile, writes with the service role and stamps the user from the verified session.
- 06Read the amount from the database, never the request, and treat only a signature-verified webhook as proof of payment. Anyone who can name their own price can pay £1 for a £1,450 booking, and the success redirect is a URL anyone can visit. Each payment also records when the customer ticked the terms, set by the server just before Stripe, as proof should a booking ever be disputed.
- 07Gave every enquiry its own payment link. Paying used to need an account holding the enquiry, and an enquiry only joined an account once its email was confirmed, so a guest, or a returning customer who enquired signed out, often had no way to pay at all. The token is a random UUID rather than the guessable enquiry id, and grants only the amount, a first name and a Pay button.
- 08Link a guest's earlier enquiries to their account on email confirmation, not signup. Matching an unverified address would let anyone register with someone else's email and read their event date, phone number and quoted price.
Outcome
Live at opuluxemoments.co.uk as the business's own site, replacing the Instagram DM process. An enquiry now arrives fully specified: event type, date and timings, guest count, venue, palette, budget, chosen styling, any backdrop the customer designed with its estimate, delivery distance and reference photos. Quotes, payments, content, policies and the emails customers receive are all run from the portal rather than by me.