Available for work — Book a 30-minute call

Security-Hardened Form-to-Database Intake

A form-to-database pipeline handling special-category personal data, rebuilt after an independent security review returned a "do not activate as designed" verdict. The hardened design verifies webhook signatures over raw bytes, grants the automation layer execute permission on exactly one database function — no connection string, no table access, no schema privileges — and resolves submissions through opaque per-record invitation tokens, quarantining anything that cannot be matched safely rather than guessing. Idempotency and revision handling make replays and corrections safe, and durable-write-before-acknowledge means the platform retries rather than silently dropping. Proven end to end, and deliberately held inactive pending formal data-protection sign-off.

Role
Designed and built
Sector
Education technology
Status
Built and tested end to end; deliberately inactive pending data-protection sign-off
Stack
Typeform, n8n Cloud, Postgres (Drizzle schema + SECURITY DEFINER function), Resend

The problem

The business needs each child's information before a course starts — health, SEND, consent, and potentially safeguarding disclosures. That is special-category personal data about children under UK GDPR Article 9. A naive form-to-database integration here isn't merely insecure; it's unlawful.

What was built

An adversarial review of the first version returned "needs rework — do not activate as designed", with six findings. Each maps to a specific fix in the rebuild:

FindingFix
Unsafe matching on email + cohort + child nameOpaque high-entropy token per booking, stored as a hash bound to the booking with an expiry; quarantine on no-match or ambiguity
Over-powered database credentialEXECUTE on exactly one function — no connection string, no table privileges, no DDL — with sanity-check queries proving the restriction
A whole data-lifecycle obligation, not a storage decisionA governance section: DPIA, lawful basis, Article 9 condition, retention across every system including logs and backups, privacy-notice update, processor terms, audited role-based access, separation of safeguarding data from teaching accommodations, and a subject-rights route with an SLA
Replay and correctionsUnique constraint on form + response token; same token with a different payload hash quarantines; a genuine correction becomes a revision with a supersedes pointer
Recovery from missed deliveries200 only after a durable write; 503 on transient database errors so the platform retries
Schema drift from question editsAnswers keyed by stable field refs, not question titles; form ID allowlisted

The hard part

Two details that separate a design that looks secure from one that is.

HMAC verified over raw bytes. Verifying a re-serialised body changes the bytes — key order, whitespace, escaping — so you're checking a signature against something the sender never signed. It's the classic way HMAC verification breaks while still passing in testing.

Least privilege, demonstrated rather than asserted. The automation role holds EXECUTE on one function and nothing else, and the SQL ends with queries confirming that the role cannot read bookings, insert, or run DDL. All resolution logic lives inside the database function, which is what makes that grant sufficient.

What can be verified

  • A real test submission flowed through end to end, authenticated first time and mapped 100% of fields, and stored correctly.
  • Six review findings each mapped to an implemented fix.
  • The system remains inactive by design.

The workflows

The n8n graph for the intake path: a Typeform trigger, a node that maps the answers to an intake payload, and a single POST to the intake endpoint. Three nodes, deliberately. (select to enlarge)
The inbound path. Three nodes is the point - the automation layer holds no database credential and makes exactly one call, so almost all of the hardening lives behind that endpoint rather than in n8n. Select the image to enlarge it — a graph this wide is not legible on a phone.
The n8n graph for the invitation path: a daily 08:00 UK schedule fetches bookings that are due, builds a send list, sends through Resend, and marks each booking as sent so it cannot be sent twice. (select to enlarge)
The invitation path that feeds the form above. The final node marks each booking as sent, which is what stops a parent being emailed the same request twice. Select the image to enlarge it — a graph this wide is not legible on a phone.

On numbers: every figure above is an artefact count or a measured technical value. No business-outcome metric, whether time saved, revenue or conversion, was captured on these engagements, so none is claimed.

On status: reflects repository evidence and platform backups, not a live systems check.

Book a 30-min call