FRQN®

Work / Diin Associations

Sleek Interio

A website that brings in customers for an interior design firm. A cost calculator asks a few questions, keeps your answers if you wander off, and hands the sales team a ranked list instead of a pile of identical form submissions.

Sole engineerJanuary to July 2026Visit live site ↗
How long an unfinished form waits
24h
Layers of spam protection
3
Packages in the codebase
6
Engineer
1
Sleek Interio site with the multi-step interior design cost calculator
Sleek Interio · as shipped

01 / The problem

Quoting an interior is not one question. It is area, rooms, how much work, what budget, by when. A plain contact form brings in rubbish, and a long form loses people in the middle. Every abandoned form is somebody the firm paid to attract and then quietly lost.

They needed three things. A form people finish. A way to get back the ones who do not. And enough information on each enquiry that the sales team knows who to ring first.

02 / What I built

It is one repository holding several pieces: a Next.js site, an Express API in TypeScript split into controllers, services and data access, and shared packages for the interface, the types and the config. Prisma describes the enquiry pipeline in Neon Postgres. Redis sits in front for anything short-lived.

The calculator quietly works out how complicated a job is and roughly what it is worth, for every submission. The sales team sees all of that in an admin dashboard where every change of status is recorded. The visitor only ever sees a tidy summary of their own quote.

03 / Deep dive

Half-finished forms are not records

An abandoned form is worth something, but it is not a customer. Writing it into the main database fills it with noise and makes everything else slower.

So the calculator saves each step to Redis and forgets it after a day. Close the tab on step four, come back tomorrow, and the form is where you left it. Only a finished, checked submission is written into Postgres. The real database only ever contains real enquiries.

  • Autosave on every step, sub-second Redis writes
  • 24h expiry clears abandoned drafts without a cleanup job
  • Postgres receives only completed submissions
CALCULATORmulti-step formREDISdrafts, 24h TTLVALIDATIONZod, full payloadPOSTGRESreal leads onlyAUTOSAVE EVERY STEPRESUME AFTER DROP-OFFSUBMITGRADUATESABANDONED AT STEP 4? THE DRAFT WAITS 24 HOURSTHE PRIMARY DATABASE NEVER SEES A HALF-FINISHED FORM
Fig 01 · Redis drafts, Postgres leads

04 / Deep dive

The browser is never trusted with anything

How the firm prices a job is the thing competitors would like to know, and an API key sitting in browser code is a breach waiting to happen. Both have the same fix: the browser only ever talks to the Next.js server.

The server forwards each request to the API and attaches the key on the way through, where nobody can read it. On the way back it strips out the internal fields, the complexity score, the estimated value, the budget band, before any of it reaches the page. Admins see the whole record. Visitors see their own quote and nothing else.

  • Server Actions used as the gateway to the API
  • Bearer-key middleware guards every protected route
  • A serializer removes internal scoring fields before they are sent
BROWSERzero secretsSERVER ACTIONBFF gatewayEXPRESS APIauth middlewareSERIALIZERstrips internal fieldsBEARER KEYFULL RECORDPUBLIC FIELDS ONLYCOMPLEXITY SCORE, REVENUE ESTIMATE, BUDGET TIER:NEVER LEAVE THE SERVER
Fig 02 · Backend-for-frontend gateway

05 / Deep dive

Spam runs into a limit

A public form is a magnet for scripts. Checking the shape of the data does not stop something submitting perfectly plausible nonsense a thousand times.

Every endpoint sits behind limits kept in Redis: ten requests a minute per address in general, and three submissions per ten minutes for the form itself, counted against the address and the email together. Every payload is checked at the door, the security headers are set, and every admin action is written down.

  • 10 requests/min general, 3 per 10 min for submissions
  • Counted on IP and email together, not IP alone
  • Validated payloads, and an audit trail on every status change
REQUESTpublic internetWINDOW 110 req / min / IPWINDOW 23 / 10 min / IP+emailZOD DTOboundary validationHANDLERaudit-loggedOVER EITHER WINDOW: 429KEYED ON IP PLUS EMAIL, A BOTNET CANNOT HIDE BEHIND ONE INBOXEVERY ADMIN STATUS CHANGE LANDS IN THE AUDIT LOG
Fig 03 · Sliding-window defense

06 / Outcomes

  • A working lead engine for a real firm, built and shipped alone
  • People who drop out halfway come back to a saved form instead of an empty one
  • Sales works from a ranked list with the history attached
  • How the pricing works has never left the server

Stack

Frontend
Next.js 15, React 19, Zustand, React Hook Form, Framer Motion
API
Node.js, Express, TypeScript, Zod, Prisma
Data
Neon Postgres, Upstash Redis
Ops
Turborepo, Clerk, Resend, Helmet, Husky

Next case study

Spotlight Furnishings