# Forms (https://lazyfrog.dev/docs/guide/forms)

{/* Generated by scripts/generate-docs.ts from services/gateway/src/guide.md. Do not edit. */}

A contact or sign-up form needs no backend: `<form method="post" action="/_lf/form/contact">` (or a
`fetch` POST with JSON, which gets `{"ok": true, "id": …}` back) stores each submission in the
environment's database, table `lazyfrog_forms.submissions` (`form`, `created_at`, `data` jsonb,
`spam`). Fields starting with `_` are instructions: `_next` is the path to send the visitor to
afterwards (otherwise back to the page with `?submitted=1`); add a hidden, empty
`<input name="_gotcha">` (it catches bots that fill every field). A post that doesn't come from a
browser on the site's own page, or that looks like spam, is kept as spam too, and is answered
like any post. Post from the page (an HTML form or the browser's `fetch`), never from the app's
server, or it lands in spam. Text fields only, 64 KB per submission; each visitor is limited to a
modest number of submissions per hour across all forms, plenty for a real visit. Read them with the `forms` tool or from the app. Add a privacy notice when a form
collects personal data. The owner reads them in the portal (Project › Messages, with a Spam tab
to check and put back), or asks you.

**Emailing the owner about new submissions.** What happens to a submission is part of the plan
(its `intake`, see "Before you build something new"): set it as agreed with `forms` `notify`:
`mode` `each` (an email for each new submission), `daily` (one summary a day, at 08:00 UTC) or
`off` (just Messages), `recipients` `"owner"` or the email addresses of the project's owner and
editors, and `form` for one form only (without it, every form). Without `mode` it says the
current setting. It mails only the project's own people with a confirmed address, never an address
you type (so a form can't be used to mail anyone else); a viewer can't be chosen. Spam never
sends an email; after 10 emails in an hour the rest of that hour come as one summary, and the
email says so. The visitor's words arrive as plain text, marked as theirs, cut short and never as
links; replying reaches the visitor when a field holds their email address. Each email has a
one-click turn-off for its recipient, the owner can change it on Project › Messages, and the
change is recorded under your name. These emails are LazyFrog's own: they work whether or not
the owner has connected an email provider for the app. LazyFrog never emails the visitor: the confirmation on the page (the `_next` page, or
the page with `?submitted=1`) is what they see, so write it to say what happens next ("Thanks,
we reply within a day"). A submission on the preview sends a test email, so you can check a
`doneWhen` item such as "a test submission arrives where the plan says". Don't build an
automatic email back to whoever filled in a public form either: anyone can type someone else's
address into it, which makes the site a way to mail strangers.
**Spam still getting through**: look at `forms list` with `include_spam`, sort with `forms`
`mark_spam` / `delete` (the owner can in Messages too); for more, the app's own handler with
Cloudflare Turnstile (its secret key through `secrets request`), knowing `/_lf/form` stays open;
`allow_countries` in routing blocks the whole site, not only the form.

**People's personal data** (a member list, contacts, bookings): import only what the feature
needs (drop notes, health details, dates of birth); ask before showing members each other's
phone numbers and let people opt out; keep such pages `noindex` and `no-store`; delete the raw
upload after importing (snapshots keep a copy for the undo history); never make a project that
holds people's data remixable (a remix copies the preview database). Moving it to the live site:
for a one-off, a confirmed production `db_query` (say that the data passes through you, the AI);
for a list the owner keeps updating, build an owner-only import page on the live site behind
`routing` `auth`, so it goes straight from their browser to the live database. Health details a
form asks for (the reason for a visit): optional and a few words, shown only on the owner's page,
never in emails, deleted on a schedule (a `cron`, with the live site always on), and said in the
privacy notice (special category data under GDPR; special personal information under South
Africa's POPIA). Pages you screenshot or query pass that data through you: say so.

**Health records and other special category data** (a clinic, a therapist, a pharmacy): the
opposite of a form's optional reason for a visit. Records are required and the clinic must keep
them (in South Africa the HPCSA's 6 years; for children until they turn 21; in the UK 8 years
for adults): an erasure request usually means restricting the record until its retention ends,
not deleting it, and the clinic decides. Build: roles enforced in the database (row-level security
for who may read clinical notes, not only hidden buttons; reception sees appointments and
contact details only), an append-only log of who opened which record, encryption in the app of
the most sensitive fields (notes, intake answers, documents) with AES-256-GCM and a key the owner
makes and pastes through `secrets request` (they keep a copy: values are never shown back, and
without it an export can't be read), one export per patient that includes their notes, and an
erasure log re-applied after any restore. LazyFrog runs in Germany; its sub-processors and the
data processing agreement are at https://lazyfrog.dev/legal: point the owner to them, and say
that a signed DPA and their own legal advice come before real patients' data.

**Marketplaces where adults meet children** (tutors, coaches, sitters): children have no
accounts; the parent books, pays and messages. A person vets every adult before they are listed
(police clearance, a teaching register, the UK's DBS), contact details stay hidden until a
booking is paid, messages are kept, can be reported, and can't be deleted by their writers, and
offenders can be suspended for good. A safeguarding page with real contacts (Childline 116 in
South Africa, 0800 1111 in the UK), reviews that don't describe the child, and images taken down
at once (new names, above). The platform's content check reads the home page at publish, never
what users post: moderation is the app's.

**Apps for children or schools** (a class quiz, a club for under-18s): the school or club is the
controller and decides (in the UK, usually public task rather than consent; a school will want a
data protection impact assessment and a processing agreement before pupils' data goes in). Follow
the ICO Children's Code: collect the least (a first name and initial, no email if it can be
avoided), private by default, nothing shown to other children unless they opt in, and then only a
made-up nickname; no analytics or tracking; a privacy notice in words a child understands; a
deletion at the end of the year (or season) that the owner runs; a way to hand a parent their
child's data. Put a preview password on before real children's data goes into the preview.
Deleted data leaves the live site at once and the undo history after its retention (below).

**Privacy on their site:** the policy is the owner's to give (you can offer a page with
placeholders; don't invent legal text). Tracking cookies need consent first (GDPR, Québec's Law
25); a functional cookie such as a language choice doesn't. What LazyFrog itself keeps about
visitors, for their policy: request logs (kept for the plan's log retention), and for platform
forms the submission with a hashed IP, the browser's user agent and the page it was sent from
(kept until the owner deletes it). LazyFrog sets no cookies on
their site.
