Most signup bugs aren't really about signups. They're about what happens first. A lead comes in, and if the system tries to enrich it before saving it, pull company data, verify the email, then saves the result, that's a problem waiting to happen. The enrichment API has one bad five minutes and every signup from that window just disappears, not stored, not logged, gone, because "enrich it" quietly became a requirement for "save it" instead of the other way around. We hit our own version of this building a lead pipeline: validate, check the email, classify, send good leads to Slack, all steps talking to something outside our control, which meant the real first decision wasn't picking a vendor. It was deciding what happens the second a signup shows up, before anything outside gets called.
The shape of it
signup payload
-> validate_input
-> postgres_insert_user
-> http_hunter_domain_search
-> http_kickbox_verify
-> classify_lead
-> switch_lead_route
-> slack_message_high_value | exit_skip_slack_low_value
-> postgres_update_user_enrichment
The order is the whole design. The lead gets saved before any external call runs, so a slow or down enrichment vendor never blocks the signup. Slack only hears about leads that actually qualify. Supabase gets updated last, once everything's known.
The steps that matter
Validate and clean the input. Trim, lowercase the email, check the format, pull the domain. Bad payloads fail here, not three steps downstream.
Save the lead first.
INSERT INTO users (email, full_name)
VALUES (:#email, :#full_name)
RETURNING email;
Supabase is just Postgres underneath, so this is a standard Postgres integration. The row exists before any third party gets asked for anything.
Enrich with Hunter, but make it optional.
https://api.hunter.io/v2/combined/find?email={{steps.postgres_insert_user.output.results[0].email}}&api_key={{secrets.hunter_io_api_key}}
Marked optional on purpose. Company data is nice to have. It should never be able to block a signup.
Verify the mailbox with Kickbox. The signal that matters is one flag: is the address free or disposable. That becomes the main input to the next step.
Classify the lead. One rule for now: free email means low_value, anything else means high_value. Deliberately simple. Company size, industry, or CRM data can slot in here later without touching the rest of the workflow.
Route to Slack based on the classification.
high_value -> slack_message_high_value
low_value -> exit_skip_slack_low_value
Notification logic lives in the workflow, not buried in application code.
Write the result back to Supabase.
UPDATE users
SET company = :#company, is_free_email = :#is_free_email, lead_type = :#lead_type
WHERE email = :#email
RETURNING email;
Same row the signup started with, now holding the full outcome.
Why this beats the scattered version
Not because scripts and queues can't do this. They can. The problem is how long that setup stays readable once a few more people have touched it. One workflow definition means the whole lead lifecycle lives in one place, every step has inspectable inputs and outputs, secrets stay managed instead of scattered, and routing logic is something you can actually read.
Natural next steps
- Fold company size or industry into
classify_leadfor better scoring - Push high-value leads into a CRM in parallel with the Slack alert
- Add retry policies around the Hunter and Kickbox calls
- Log every outcome to an audit table for reporting
Because it's definition-driven, these are additions, not rewrites.
Top comments (0)