A contact form that is easy for bots is a part-time job for whoever owns the inbox. A form that is hostile to humans loses the hire or the deal. Good intake design sits in the middle: friction for abuse, clarity for real people.
It sounds like a small piece of UI to get right. In practice a careers or contact form touches security, GDPR, storage, and conversion at the same time — which is exactly why it's usually the least-loved part of a site and the one that quietly costs the most when it's wrong. Here is the checklist we use on careers and contact flows for client and studio sites, and the reasoning behind each item.
Ask only for what you will use this week
Every field has a cost. Each one adds a decision the visitor has to make, a validation rule someone has to maintain, and a piece of personal data someone now has to protect and eventually delete. For contact:
- Name, email, message — usually enough
- Company / phone — optional unless sales truly dials every lead
- Subject — useful if the inbox is shared
For careers:
- Role (pre-filled from the listing)
- Name, email, phone
- Cover note short enough to read on mobile
- CV upload with a hard size and type limit
Long "tell us your life story" textareas invite paste-spam and abandon on mobile. They also produce the worst kind of data: long, unstructured, and rarely read past the first two lines by whoever triages the inbox. If a field exists because "it might be useful someday," it isn't earning its place — either give it a real owner who will act on the answer, or cut it. A short form that gets finished beats a thorough one that gets abandoned on the third field.
Consent that is real GDPR, not theatre
Unchecked marketing checkboxes buried in grey text fail both law and trust. The failure mode here isn't usually malicious — it's a form built quickly, with a single "I agree to the terms" checkbox doing double duty for three different things: replying to the message, storing the data, and adding the person to a newsletter. That's not consent, it's one checkbox pretending to cover three separate decisions, and it puts the studio and the client on the wrong side of GDPR the moment someone complains.
For EU-facing sites:
- Separate necessary processing (to reply) from optional marketing
- Link privacy policy next to the control
- Do not pre-tick optional consent
Record the consent timestamp server-side with the submission. Support will thank you later — the question "did this person actually agree to be emailed?" comes up more often than teams expect, usually months after the form was filled in, and "we assume so" is not an answer that holds up. A timestamp and a stored consent state turn that into a two-second lookup instead of a guess.
Anti-spam that users do not see
Layer defences; do not rely on one. Every individual technique below is weak on its own — a determined bot operator can usually beat any single layer within a week if it's the only thing standing between them and the form. Stacked together, they're cheap to implement and expensive to bypass, which is the trade-off that actually matters:
- Honeypot field hidden from assistive tech and CSS
- Honeytime — reject submits faster than a human can read the form
- Rate limit by IP on the API
- Server-side validation with a schema (zod or equivalent)
CAPTCHAs are a last resort. They hurt conversion and accessibility; use them when abuse is already measured, not by default. The order matters here — teams tend to reach for a CAPTCHA first because it feels like the "serious" solution, then discover it's added friction for every legitimate applicant while a slightly patient bot still gets through. The honeypot-plus-honeytime combination alone stops the overwhelming majority of scripted spam, because most bots fill in every field and submit in under a second — no human does either.
Uploads without melting storage
CV and attachment endpoints need:
- MIME allow-list (PDF, DOC/DOCX — not "any file")
- Max size enforced in browser and server
- Private storage bucket; never a public URL guessable by ID
- Virus scanning or at least quarantine + admin-only download
Never email the raw file to a shared inbox as the only copy. Store it, then notify with a link behind auth. This one is easy to get backwards during a rushed build: emailing the file feels like the fast path, and it works fine for the first few submissions. It stops working the moment someone uploads a 40MB file, or the inbox has a size limit, or the recipient who's supposed to review CVs is on leave and nobody else has access to their email. A storage bucket with an admin link degrades gracefully in all three cases; a shared inbox does not.
An unguessable URL matters more than it looks like it should. "Guessable by ID" usually means a sequential filename or a predictable path — which turns one leaked link into every applicant's CV being reachable by anyone who increments a number. It costs nothing extra to generate a random token instead, and it closes an entire class of accidental exposure.
Success states beat silent failures
After submit:
- Redirect to a dedicated success page or show an unambiguous confirmation
- Tell the user what happens next and when ("We reply within one business day")
- Keep the message if validation fails — do not wipe a carefully written cover letter
On the admin side, default status should be new / unread with filters. If everything lands as "processed," nothing gets processed. This is a smaller design decision with a larger consequence than it looks: a status field that defaults to "read" or shows no unread count at all is invisible pressure removed from whoever's supposed to triage the inbox. Nothing looks urgent, so nothing gets acted on until a candidate or a prospect follows up asking why they never heard back.
On the visitor side, the failure mode is just as costly in the other direction. Someone who spends fifteen minutes writing a cover note, hits submit, sees a validation error, and finds the textarea has gone blank will not write it a second time. They'll either give up or submit something worse. Preserving form state across a failed validation is a small amount of client-side work that directly protects the quality of what actually lands in the inbox.
Common mistakes we see
A few patterns show up repeatedly across contact and careers forms, independent of industry:
- Validation that only runs client-side. Anything enforced only in the browser is a suggestion, not a rule — it's trivial to bypass with a direct API call, which is exactly what spam scripts do.
- One field doing the job of consent, marketing opt-in, and terms acceptance at once. It's faster to build and impossible to defend if anyone asks what the person actually agreed to.
- No rate limiting on the API endpoint, even when the form itself has anti-spam measures. The form's defences don't apply if the endpoint accepts direct requests.
- Success and error states that look identical, or a generic "Something went wrong" with no indication of what to fix — which turns one wrong phone number format into an abandoned submission.
None of these are hard to fix individually. They tend to accumulate because a contact form is treated as a five-minute task late in a build, when it's really a small piece of the product with security, legal, and conversion requirements of its own.
What we deliberately skip
Not every defence is worth adding by default, and that's a real decision, not laziness. We don't ship CAPTCHA on day one — it's a cost paid by every legitimate visitor to solve a problem that hasn't been measured yet. We don't ask for more identity verification than the flow needs; a careers form does not need a phone-number confirmation code to filter spam when a honeypot and rate limiting handle it for free. The rule we apply is simple: add a layer when the data shows abuse, not before, and prefer the layer that costs the abuser more than it costs the real applicant.
Measure completion, not just opens
Track:
- Form start → submit success
- Drop-off by step (multi-step wizards)
- Spam rejects vs human rejects
If completion falls after you add a field, that field needs a business owner — or deletion. This is the feedback loop that keeps a form honest over time. Forms tend to grow one field at a time, each addition individually reasonable, each one shaving a percentage point off completion that nobody notices because nobody's watching the trend. Splitting spam rejects from human rejects also matters on its own: a spike in human validation errors usually means the form is asking for something in the wrong format, while a spike in spam rejects means the anti-abuse layer is working as intended.
Stereasoft ships contact and careers intake with Resend notifications and Supabase-backed admin review. The goal is simple: serious people get through; the inbox stays usable on Monday morning.
