For months, everyone who wrote to us from flouvia.com traveled to a Make scenario. It worked, until we audited it: the webhook URL was visible in the browser, nothing validated on the server and, if Make failed, the message simply vanished. For a firm that builds bespoke software, it was the example of what we tell our clients not to do.
What was wrong
- The secret wasn't secret. The form called the Make URL directly with fetch. Anyone opening the browser's developer tools could copy it and flood our scenario with junk.
- Nothing validated on the server. The only validation was the form's own, which any script skips.
- Failures were silent. If the scenario errored or ran out of operations, the lead was stored nowhere.
- The logic lived outside the repository. Changing a form field meant remembering to update the scenario by hand.
The new flow, step by step
We rewrote it like any other part of the product: in the same repository, typed, tested and deployed with the site.
- The browser posts to our own API (/api/contact), never to a third party. Every submission carries a unique key generated in the browser.
- The API filters before touching anything: it only accepts requests from our own domain, rate-limits per IP and validates every field against a schema. The form options come from the same file the validation uses, so they cannot drift apart.
- Bots are dropped silently. A hidden field a person never fills and a minimum fill time separate people from scripts. The bot gets a normal response, so it doesn't learn to dodge the filter.
- Store first, notify second. The lead is written to Postgres before any email is sent. If the email fails, the record keeps the error and can be retried from our internal console.
- Two emails, one provider: a notice to the team, which replies straight to whoever contacted us, and a confirmation to that person in their language. Everything goes through a single email module on Resend.
The key that makes it safe: end-to-end idempotency
The least visible part is the most important. The key the browser generates travels to the database, where it is unique, and from there to each email, which carries its own stable key. The result:
- If the network fails and the person submits again, no duplicate lead is created.
- If we retry a notice that actually went out, the email provider recognizes it and doesn't send it twice.
- If the person changes anything in the form, a new key is generated: it's a new submission.
What we gained
| Before (Make) | Now (our own code) | |
|---|---|---|
| Where the lead lives | In the scenario, if it ran | In our database, always |
| If something fails | Lost without notice | Stored with the error and retried |
| Spam | No filter | Origin, per-IP limit, honeypot and minimum time |
| Changing the form | Code and scenario separately | A single options file |
| Tests | Manual | Automatic on every deploy |
So is Make bad?
No. Make is a good tool to connect applications that already talk to each other and to validate an idea quickly. The problem appears when a flow becomes critical to the business: then it needs to live where your product lives, with the same security rules, the same tests and the same history. A lost lead isn't a technical error; it's a sale you never knew existed.
If an automation is critical to your operation, it deserves the same care as the rest of your software.
It's the same principle we apply in Cord and in the systems we build for our clients: whatever moves money or customers lives in owned code, audited and with its own safety net.