Supabase backend
Supabase backend
contact edge function
Receives the website’s contact and estate-scan forms, validates them, and emails the team. Replaces the Google Form, which collected too much junk.
What it checks
| Check | Rule |
|---|---|
| Honeypot | the hidden website field must be empty |
| Time trap | at least MIN_FILL_SECONDS (default 3) between form render and submit |
| Rate limit | 5 submissions per IP per 10 minutes |
| valid syntax, not a consumer mailbox (Gmail, Outlook, Proton, QQ…), not disposable | |
| Name | first and last name, letters only, no keyboard mash, accents and hyphens fine |
| Company | length and junk check, optional |
| Message | 20–5000 chars, max 2 links, no marketing phrases, not shouting, not gibberish |
The estate-scan form only needs a work email; the contact form needs name and message too.
Rejections return 422 with plain-English messages. The precise reason is logged, never returned, so a spammer can’t tune against the rules.
Deploying
supabase link --project-ref <your-project-ref>
supabase functions deploy contact --no-verify-jwt
--no-verify-jwt matters: the website posts anonymously, with no Supabase auth
token.
Secrets
Set these once, from the SMTP credentials file. Never commit them.
supabase secrets set \
SMTP_HOST="<endpoint from the credentials file>" \
SMTP_PORT="587" \
SMTP_USER="<username>" \
SMTP_PASS="<password>"
Inquiries go to abi@bimigrations.com and les@bimigrations.com by default.
Set the MAIL_TO secret (comma-separated) only to change that list.
Optional settings:
| Variable | Default | Purpose |
|---|---|---|
AUTO_REPLY |
true |
send the sender a thank-you |
ALLOWED_ORIGINS |
the live site plus localhost:4321 |
CORS allowlist |
MIN_FILL_SECONDS |
3 |
time trap threshold |
ALLOW_PUBLIC_DOMAINS |
false |
accept Gmail etc. — testing only |
MAIL_FROM |
BI Migrations <abi@bimigrations.com> |
sender; must be SES-verified |
MAIL_TO |
abi@, les@bimigrations.com |
where inquiries land, comma-separated |
MAIL_FROM defaults to BI Migrations <abi@bimigrations.com>, which is the
address verified in SES. Only set the secret if that changes — and whatever you
set must be verified in SES, or every send fails.
Testing it
# should succeed
curl -i -X POST "$FN_URL" -H 'content-type: application/json' \
-d '{"form":"contact","name":"Jane Okafor","email":"jane@acme-analytics.com",
"company":"Acme Analytics","message":"We run 400 Tableau workbooks and are scoping a move."}'
# should be rejected: consumer mailbox
curl -i -X POST "$FN_URL" -H 'content-type: application/json' \
-d '{"form":"contact","name":"Jane Okafor","email":"jane@gmail.com","message":"We are scoping a migration project."}'
Unit tests
deno test supabase/functions/contact/validate_test.ts
Covers the validation rules with no network or SMTP involved.
Show us your estate.
A read-only metadata export is enough to start. No platform access required, mutual NDA as standard.