A booking page needs somewhere to put a booking. For the volume a personal site sees — a few a week at best — reaching for Postgres feels absurd. So the instinct is to write a row to a file and move on.
On Vercel, and on serverless generally, that instinct is wrong in a specific and nasty way. The filesystem is read-only apart from /tmp, and /tmp belongs to a single invocation. Write to it and the call succeeds. Read it back on the next request and the file is gone — different instance, clean disk.
That is the part worth dwelling on. It does not throw. There is no error to catch and no log line to notice. Bookings arrive, get written, and evaporate, and you find out when somebody asks why nobody called them back.
The question underneath
Once "a file" is off the table the reflex is to reach for a database, and that is where a small feature quietly acquires a schema, a migration story, a connection pool and a monthly bill. Worth asking first: what does the record actually need to do?
- Survive long enough to be acted on.
- Be searchable later, when someone asks what was agreed.
- End up in the calendar, because that is where the booking has to be to matter.
A mailbox does all three. So the booking is an email with the invite attached — searchable in Gmail, and already in the calendar the moment it is opened. No schema, nothing to migrate, nothing to pay for.
// Two emails, both carrying the same .ics as METHOD:REQUEST —
// which is what makes a mail client offer "add to calendar"
// rather than showing a file attachment.
const attachments = [{ filename, content: icsBase64 }]
await send({ from, to: [host], reply_to: guest, subject, html, attachments })
await send({ from, to: [guest], reply_to: host, subject, html, attachments })What this is not
This is not an argument against databases. It is an argument against adding one before the thing it enables is needed. The moment bookings need listing, filtering or cancelling from the site itself, the mailbox stops being enough and a real store earns its place.
The failure mode that matters is not the one that errors. It is the one that returns 200 and loses the data.
