Skip to content
Balwan Bamniya, home
Menu
Back to work

Case study

LeadFlow

An all-in-one CRM, booking, quoting and payments app for small service businesses, with role-based portals, verified online payments, and AI-drafted follow-ups streamed into the lead view.

#Next.js 16#React 19#TypeScript#Drizzle#Neon Postgres#Razorpay#Vercel AI SDK#Resend
LeadFlow landing page with the headline "Run your service business from one place" and four feature cards.

The problem

Cleaners, photographers, tutors and consultants usually run their business across WhatsApp, email, a calendar and a spreadsheet. Enquiries go cold, quotes are retyped into invoices, and customers have to ask for every update.

LeadFlow puts that whole loop in one product, with three roles: owners and staff work the pipeline, and customers get a portal to see appointments, accept quotes and pay invoices. I scoped AI to one job where it clearly saves time — summarising an enquiry and drafting the follow-up — rather than spreading it across the app.

The revenue loop

  1. LeadCaptured into a pipeline
  2. Follow-upAI-drafted, human-sent
  3. AppointmentDaily reminder job
  4. QuoteDraft → sent → accepted
  5. InvoiceOne per accepted quote
  6. PaymentRazorpay, verified twice

Architecture

One Next.js app, no separate backend

  1. Next.js 16 App Router

    • Server componentsRead through a role-checked data layer
    • Server actionsZod-validated writes + audit log
    • Route handlersAI, payments, uploads, cron

    Drizzle ORM

  2. Services

    • Neon Postgres15 tables, CHECK constraints
    • RazorpayOrders, verify, webhook
    • OpenAIgpt-4o-mini via AI SDK
    • ResendReact Email templates
    • CloudinarySigned direct uploads

Hard problems

1. Payments that can’t be spoofed

The browser reports a successful checkout, but the server never trusts it. Verification checks that the invoice belongs to the signed-in customer and that Razorpay’s HMAC signature matches. A separate webhook validates the raw request body and marks the invoice paid even if the customer closes the tab before the client callback fires.

const invoice = await db.query.invoices.findFirst({
  where: eq(invoices.id, invoiceId),
  with: { customer: true },
});
if (!invoice || invoice.customer?.userId !== user.id) {
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

const isValid = validatePaymentVerification(
  { order_id: razorpay_order_id, payment_id: razorpay_payment_id },
  razorpay_signature,
  process.env.RAZORPAY_KEY_SECRET ?? "",
);
if (!isValid) {
  return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}

app/api/razorpay/verify/route.ts

Unique indexes on the Razorpay payment ID and on quote_id (one invoice per quote) back this up at the database level.

Customer portal invoice marked Paid, showing line items, tax and total.

2. AI drafts grounded in real context

The draft-email route loads the lead and the customer’s five most recent messages, then streams plain text straight into the UI, so the first words appear quickly. Staff edit and send the draft themselves — the model never contacts a customer directly.

const result = streamText({ model: openai("gpt-4o-mini"), prompt });
return result.toTextStreamResponse();

app/api/ai/draft-email/route.ts — the prompt includes status, original enquiry and message history

3. One codebase, three audiences

A requireRole() helper in a server-only data layer guards every page, action and route handler, redirecting users to the area their role allows. Customers only ever query rows linked to their own user ID.

Owner leads board in Kanban view with New, Contacted, Quote Sent, Won and Lost columns of seeded leads.

Outcome

  • A deployed, seeded demo with separate owner, staff and customer accounts, covering the full loop from enquiry to paid invoice in Razorpay test mode.
  • Money stored as numeric(12,2) with database CHECK constraints on amounts and quantities.
  • Quote and invoice totals computed on the server, never trusted from the form.
  • Six React Email templates: welcome, lead follow-up, appointment confirmation and reminder, quote sent, invoice sent.

What I’d change

This was built fast, and the code review I did afterwards is the most useful part of the project: