The Problem: Onboarding Small Businesses Without a CRM

When AiSalonHub launched, we faced a classic chicken-and-egg problem. Salons wouldn't join without customers browsing, and customers wouldn't browse without salons listed. Traditional CRM solutions like HubSpot or Salesforce were overkill for a niche directory, and they'd add $50-200/month in costs before we had any revenue.

We needed a lightweight lead capture and onboarding system that could:

- Let salon owners express interest via a simple form

- Track their onboarding progress (profile creation to service listing to verification)

- Send follow-up reminders automatically

- Scale to thousands of salons without per-seat licensing costs

Our solution? Build the entire lead management pipeline on top of EmDash's CMS and Cloudflare Workers — no external CRM, no third-party API costs, just D1 plus Workers plus transactional email.

The Lead Capture Form

The entry point is a multi-step form embedded directly in AiSalonHub's EmDash page using Portable Text custom blocks. When a salon owner fills out the form, a Cloudflare Worker receives the submission and:

1. Creates a new entry in the `ec_lead_capture` table with status `new`

2. Generates a unique onboarding token (a ULID hashed with the salon's email)

3. Sends a confirmation email via the Worker's built-in fetch() to a transactional email API

4. Updates the page to show a success message with next steps

The form itself asks for just four fields: salon name, email, phone, and ZIP code. Keeping it minimal maximizes conversion — we found that asking for more than four fields dropped submission rates by over 40% in A/B testing.

```typescript

// Worker handler for lead capture

async function handleLeadCapture(request: Request, env: Env): Promise<Response> {

const data = await request.json();

const leadId = generateULID();

const token = await generateToken(data.email);

// Insert into D1

await env.DB.prepare(

`INSERT INTO ec_lead_capture (

id, salon_name, email, phone, zip_code, status, token, created_at

) VALUES (?, ?, ?, ?, ?, 'new', ?, datetime('now'))`

).bind(leadId, data.salonName, data.email, data.phone, data.zipCode, token).run();

// Send confirmation via email API

await fetch('https://api.resend.com/emails', {

method: 'POST',

headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}` },

body: JSON.stringify({

to: data.email,

subject: 'Welcome to AiSalonHub - Complete Your Profile',

html: `<a href="https://aisalonhub.com/onboard/${token}">Finish setup</a>`

})

});

return new Response(JSON.stringify({ success: true, leadId }));

}

```

Onboarding Pipeline as State Machine

The lead status field acts as a lightweight state machine tracked entirely in D1:

```

new -> contacted -> onboarding -> verified -> listed

\\-> unresponsive (30 days) -> archived

onboarding -> stalled (7 days) -> reminder_sent -> onboarding

```

A cron worker runs every 6 hours and queries D1 for leads in each state. It processes transitions using simple SQL updates — no message queues, no event buses, no Kafka topics:

```sql

-- Find stalled leads and send reminders

UPDATE ec_lead_capture

SET status = 'reminder_sent',

updated_at = datetime('now'),

reminder_count = reminder_count + 1

WHERE status = 'onboarding'

AND updated_at < datetime('now', '-7 days')

AND reminder_count < 3;

```

Leads that receive three reminders without responding are automatically archived after 30 days. The entire workflow is declarative — we can add new states or transition rules by simply updating the cron worker's SQL queries, with zero downtime.

The Salon Dashboard

Once onboarded, salon owners get a lightweight dashboard built as an Astro SSR page behind simple token-based auth. No password, no OAuth, no magic link flow — the onboarding token from the email doubles as a session key stored in a Cloudflare KV session. The dashboard lets them:

- View their AiSalonHub profile as visitors see it

- Update service listings and prices

- See how many views their profile has received in the past 7 days

- Submit photos and additional business details

The dashboard is intentionally minimal — it's a lead management tool disguised as a CMS. Salon owners don't need a complex admin panel; they just need to keep their info current and see that their listing is driving traffic. The view count alone has been the strongest motivator for completing onboarding.

Analytics Feedback Loop

The lead capture system feeds into a simple analytics pipeline for the admin panel:

```sql

-- Weekly lead summary for admin dashboard

SELECT

COUNT(*) AS total_leads,

SUM(CASE WHEN status = 'listed' THEN 1 ELSE 0 END) AS converted,

ROUND(AVG(julianday('now') - julianday(created_at))) AS avg_onboarding_days,

SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) AS dropped_off

FROM ec_lead_capture

WHERE created_at > datetime('now', '-30 days');

```

This data powers a small admin dashboard showing conversion rates, average onboarding time, geographic distribution of leads mapped by ZIP code, and weekly trend charts — all without a single third-party analytics tool. The entire analytics view is an Astro page that renders D1 query results directly into Chart.js graphs served from the Workers assets.

Why This Matters

The AiSalonHub lead capture system demonstrates a broader pattern: for niche SaaS products, you don't need enterprise CRM tools at launch. By building lead management directly into your content platform, you eliminate integration overhead, reduce monthly costs to near zero, and keep the entire user journey in one database.

This serverless-CRM-as-CMS approach saved roughly $1,200 per year in SaaS subscriptions during the first year. The total development time was under a week, and the operational cost is basically zero (the cron worker and D1 queries are well within Cloudflare's free tier). For any early-stage directory or marketplace, it's worth asking: do you really need Salesforce, or do you just need a D1 table with a cron worker and some well-crafted SQL?