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.
- How long an unfinished form waits
- 24h
- Layers of spam protection
- 3
- Packages in the codebase
- 6
- Engineer
- 1

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