Funnel anti-spam rate-limit
Use layered form protection to reduce spam and abuse while keeping genuine patient enquiries and bookings easy to complete.
The lead funnel applies three anti-spam guards before recording a submission. All are tenant-configurable per funnel.
The three guards
1. Per-IP rate limit
- Default: 5 submissions per 10 minutes per IP
- Counts submissions to the same funnel from the same IP within the window
- Reject reason:
ip_rate_limited
2. Per-email rate limit
- Default: 3 submissions per 1 hour per email
- Counts submissions to the same funnel from the same email within the window
- Reject reason:
email_rate_limited
3. Meta-claim vs evidence
- When the submission's
utm_sourceis one offacebook/meta/instagram/fb/ig, orutm_mediumispaid_social/cpc_social, - We require an
fbclidto be present - If not, reject reason:
meta_claim_without_fbclid
This catches direct bot submissions that fake Meta attribution
to claim ad credit — bots typically don't replay an fbclid.
Where IP is captured
The frontend calls the funnel-submit Edge Function, which
reads the client IP from these headers (in order):
x-forwarded-for(first entry)cf-connecting-ipfly-client-ipx-real-ip
The IP is then passed to submit_lead as _ip_address. The
RPC's guard reads it. If no IP is available, IP-based guards
are skipped but email + claim-vs-evidence guards still run.
Tuning thresholds
Per funnel, on lead_funnels:
rate_limit_ip_window_seconds/rate_limit_ip_maxrate_limit_email_window_seconds/rate_limit_email_maxrequire_fbclid_for_meta_traffic(bool)
Edit via SQL or via /practice/leads → funnel detail.
Set rate_limit_*_max = 0 to disable that guard entirely.
Visibility — the rejections table
Rejected submissions are recorded in
funnel_submission_rejections with:
- Tenant + funnel
- IP, email, UTM context, fbclid
- Reason
- User-agent
- Timestamp
Read via:
select * from public.list_funnel_rejections(100);
Or via the manager UI on /practice/leads → Rejections panel.
When to tune
- Patient complains they got blocked — check rejections for their email / IP. Genuine patients hitting a limit means thresholds are too tight — relax them.
- Bot wave — if rejections spike, tighten thresholds temporarily.
- Meta-attributed traffic gone — if
fbclidis filtered by a recent Meta API change and legitimate ad traffic is being rejected, setrequire_fbclid_for_meta_traffic = falsetemporarily while Meta sorts it out.
What anti-spam doesn't catch
- Sophisticated bots that replay a real
fbclidand use one IP per submission — the rate limits won't fire. Add a CAPTCHA to the funnel (we don't have one yet — sized for a future slice if needed). - Slow-roll attacks below the threshold — by definition, this guard is for fast / repeated patterns.
- Internal abuse — staff accidentally testing the funnel repeatedly doesn't get an exemption. Lift the threshold per funnel if your team needs to test.
Frontend UX
When a submission is rejected, the patient sees a friendly per-reason message:
email_rate_limited→ "You've recently submitted this form. Please wait a few minutes before trying again — or check your inbox for our earlier reply."ip_rate_limited→ "Too many recent submissions from your network. Please wait a few minutes and try again."meta_claim_without_fbclid→ "We couldn't verify your visit came from our ads. Please tap our most recent ad link to retry, or contact the clinic directly."
No raw error code is shown — the patient gets actionable language.