All Works
Full-stack engineer (client project): design, build, operate/2026

EMS Consulting

A recruitment consultancy's site, CRM and inbox on Postgres

EMS places engineers and site teams with construction and infrastructure companies. I rebuilt their public site as a lead engine, then built the back office they run on: candidate intake, employer requirements, a CRM with its own email inbox, invoicing and a recruiter workspace.

Live · emsconsulting.in For: Recruitment consultancy, Delhi NCR
6.31 MB → 32.4 KB
inbox payload
988 → 198 ms
email ingest, mean
4.90 → 1.20 ms
inbox list query
4 → 1
threads from 12 simultaneous deliveries, before and after the fix
EMS Consulting's home page with a callback form

// The Problem

The consultancy needed its website to bring in candidates and employers, and a back office to track them. Enquiry email went to a personal inbox, with no shared record of who had been contacted.

The constraint for the email system: it had to cost nothing to run.

// How It's Built

  1. Cloudflare Email Routing

    Inbound for info@, free, messages up to 25 MiB

  2. Email Worker

    Parses the message and uploads attachments straight to storage

  3. Signed webhook

    HMAC, constant-time check, dedupe on Message-ID

  4. One Postgres function

    Dedupe, threading, insert and unread count in one transaction

  5. CRM inbox

    A view with a LATERAL join returns 100 rows, not 2,943

Inbound mail lands in Postgres in one round trip, with attachments kept out of the request.

// Decisions & Trade-offs

  • 01

    Free inbound, paid-for nothing

    Inbound runs on Cloudflare Email Routing and outbound on Resend's free tier. Cloudflare's own sending needs a paid plan, so it isn't used.

  • 02

    Attachments skip the webhook

    The serverless request body tops out near 4.5 MB while Cloudflare accepts 25 MiB messages, and the large messages (CVs) are exactly the ones worth keeping. The Worker writes attachments to storage directly.

  • 03

    Contacts keyed on the header From

    Many providers send from a unique bounce address per message, which was creating a new contact for every email. Matching on the header From fixed it, since SPF and DKIM have already been checked by then.

  • 04

    Workspace routing by hostname, outside middleware

    The recruiter workspace lives on its own subdomain through a host-matched rewrite, so public pages never pay for an auth round trip. It has its own login because session cookies are host-only.

// Problems Worth Telling

  • 01

    Zero CVs collected

    After launch the client reported that CVs weren't arriving. The form allowed 5 MB but Server Actions default to a 1 MB body, so every real CV was rejected before my handler ran: the database held zero submissions. Raised the limit and confirmed with real uploads the same day.

  • 02

    Concurrent deliveries corrupted threads

    Reproduced with 12 simultaneous deliveries: 4 threads instead of 1 and an unread count of 2 instead of 12. A read-then-update lost increments and thread creation raced. Ingest is now one Postgres function with an advisory lock per sender: 1 thread, 12 messages, unread 12.

  • 03

    Opening an unread thread crashed the page

    Marking a thread read called revalidatePath during render, which Next 16 throws on. It went unnoticed because the page sits behind a login. Read-marking now runs after the response.

// Quality

  • Performance measured before and after, never estimated: the list, the ingest path and the thread view each have their numbers in the commit that changed them.
  • Row-level security with public reads only for open jobs and active services; every lead table is server-only.

// Where It Stands

  • Live at emsconsulting.in. The consultancy runs candidate intake, its inbox, invoicing and submission packs on it.

// Stack

Next.js 16React 19Supabase (Postgres, Auth, Storage)Cloudflare Email Routing and WorkersResendpdf-libZod
Next case studySalon operations app

Building something similar?

Let's talk