My portfolio is a static Astro site sitting in an S3 bucket. There is no server running anywhere. So the day I want a contact form on it, I hit the obvious question: where does the POST go? Here is the path I would build, front to back, without standing up a box to babysit.
The “just stand up a backend” trap
The default advice for a contact form is to run a small API. An Express server, a container, somewhere for the POST to land. For a portfolio site that gets a handful of submissions in a month, that is a server you now patch, monitor, and pay for around the clock so it can do almost nothing.
I don’t want a box running 24/7 to catch maybe five messages. So I would have the form post to code that only exists while it runs.
The pieces
- Astro builds the static frontend. The form is plain HTML with a fetch on submit.
- A Cloudflare Worker (
workers/contact.js) is the endpoint. It wakes up on the request, does its job, and disappears. - Cloudflare Turnstile checks the submission is not a bot before anything else runs.
- Notion stores each lead as a row. Free tier, no record cap to worry about.
- Resend emails me from
noreply@the moment a lead comes in.
How a submission would travel
Someone fills the form. The browser sends the fields plus a Turnstile token to the Worker URL. The Worker verifies the token with Cloudflare, and if that fails it stops right there. On a pass, it writes a row to the Notion database and fires an email through Resend. The visitor gets a success state. Total round trip lands well under a second.
Every secret (the Turnstile key, the Notion token, the Resend key) would live in the Worker as an encrypted secret, never in the frontend bundle.
What it would cost
At my volume, nothing. The Worker free tier covers 100,000 requests a day. Notion is free. Resend’s free tier sends 3,000 emails a month. The only fixed cost is the S3 bucket, which rounds to pennies.
Compare that to a small always-on server. Even the cheapest instance is a few dollars a month to sit idle between submissions.
Where this breaks down
This is not the setup for a high-traffic app with auth, sessions, and heavy writes. Workers have CPU limits per request, and Notion is a fine store for leads but a bad database for anything relational. If the form grew into a full product, I would move the store to a real database and keep the Worker as the entry point.
For a static site that needs to catch leads and email you, though, this is hard to beat. You get real capture with no server to babysit.