Skip to content

In-house buildA system we built for ourselves and run on miniailabs.in. Not client work; the status section says which parts are switched on today.

Workflow Automation

In-house build

Lead Pipeline and Admin Behind Every Form on This Site

Every form on miniailabs.in, from the contact page to the chat handoff, posts to one serverless intake that we built for ourselves. With the database attached, a lead is written once, in a single transaction with its history and its notifications; the visitor gets a reference only after that write commits; and email goes out through an outbox that retries with backoff and does not send twice. The team works the leads in a small admin behind Cloudflare Access.

Runs on
Behind every form on miniailabs.in; the admin is private
Service
Workflow Automation
Built
October 2026, alongside the site redesign; the database, scheduled worker and Access switch on at release
Live today
1 of 4 parts · 18 Oct 2026

Check it yourself

In-house build

What you will see when you contact us

  • A reference on the thank-you screen that the team can find your enquiry by
  • The same reference again if the form is sent twice
  • A reply that already knows what you asked the chat assistant

The brief

Why we built it

A contact form that only emails an inbox works until an email fails: the mail service has an outage or a key expires, and the enquiry exists nowhere else. Double clicks and patchy mobile connections create duplicates, and a chat handed to a person arrives without the conversation. We wanted the properties we design into client systems: the lead is stored before the thank-you appears, a failed notification is retried and then raised to a person, a retry never creates a second lead or email, and the people working the leads sign in through an identity provider, not a shared password.

Outbox run

One alert, one failure, one retry

How a notification moves through the outbox when the email service fails once. The lead was safe from the second step.
Sample data
Form submitsubmission keyDone
D1 batchone transactionDone
Reference200 after commitDone
Drainclaim rowDone
Attempt 1provider errorFailed
Backoff~2 min + jitterRetrying
Attempt 2sent, same keyDone
Dead letterafter 6 attemptsIdle

Sample data. A submit is stored in one D1 batch and the reference returned. Attempt 1 of the team alert fails, the row waits about two minutes plus jitter, and attempt 2 is sent with the same idempotency key. After six failures a row takes the dead-letter branch.

Architecture

How the pieces connect

  • Site forms· channel
  • Chat handoff· channel
  • Intake checks· processing
  • Outbox drain· processing
  • Access + roles· processing
  • D1 leads + outbox· system of record
  • Audit log· system of record
  • Team inbox· person
  • Admin user· person
Show the architecture diagram

Architecture of the Lead Pipeline and Admin Behind Every Form on This Site in-house build: Site forms → Intake checks (JSON + submission key); Chat handoff → Intake checks (transcript); Intake checks → D1 leads + outbox (one transaction); D1 leads + outbox → Outbox drain (due rows); Outbox drain → Team inbox (alert email); Admin user → Access + roles (Access sign-in); Access + roles → D1 leads + outbox (role-scoped reads); Access + roles → Audit log (every change).

Diagram with 9 parts: Site forms (source), Chat handoff (source), Intake checks (processing), Outbox drain (processing), Access + roles (processing), D1 leads + outbox (system of record), Audit log (system of record), Team inbox (human), Admin user (human).

Connections:

  • Site forms → Intake checks (JSON + submission key)
  • Chat handoff → Intake checks (transcript)
  • Intake checks → D1 leads + outbox (one transaction)
  • D1 leads + outbox → Outbox drain (due rows)
  • Outbox drain → Team inbox (alert email)
  • Admin user → Access + roles (Access sign-in)
  • Access + roles → D1 leads + outbox (role-scoped reads)
  • Access + roles → Audit log (every change)

Connections

  1. Site forms → Intake checks (JSON + submission key)
  2. Chat handoff → Intake checks (transcript)
  3. Intake checks → D1 leads + outbox (one transaction)
  4. D1 leads + outbox → Outbox drain (due rows)
  5. Outbox drain → Team inbox (alert email)
  6. Admin user → Access + roles (Access sign-in)
  7. Access + roles → D1 leads + outbox (role-scoped reads)
  8. Access + roles → Audit log (every change)

Intake

Checks before anything is stored

Each request passes the cheap checks first: same origin, a JSON content type, a 16 KB body limit and a honeypot field that bots fill and people never see, accepted silently and dropped. The schema then validates every field with field-level messages; a submission that declines consent is rejected, and the time of consent is stored with the lead.

Duplicates are recognised before the rate limit, so a retry never counts against the visitor, and a visitor receives at most one acknowledgement email a day. Logs carry a request id and the outcome, never a name, email, phone number or message.

Outbox

Emails as rows, sent after the response

The lead, its first event and its outbox rows are written in one D1 batch, and the reference goes back only after that batch commits. An outbox row holds just the lead id and reference: the drain loads the lead when it sends, so the queue keeps no copy of anyone’s details, and erasing a lead stops its pending mail.

Three things drain the outbox: the request itself after responding, a scheduled worker every five minutes and an owner pressing replay. A compare-and-set claim lets them overlap without double-sending, and an idempotency key on every email covers a crash between sending and marking sent. A failure waits two to the power of the attempt count in minutes, plus jitter; after six attempts, or an error that retrying cannot fix, the row is dead and a dead-letter alert without personal data goes to the team.

Failure handling

What happens when each part fails

Each case has a test behind it in the repository.

  • Email service down

    The lead is already stored; the outbox retries and the admin shows the row as failed with a redacted error.

  • Database unreachable

    The team is emailed directly and no reference is shown. If that fails, the browser falls back to a hosted form service, and if every route fails the form shows an error, not a thank-you.

  • Double submit or a resend

    The key returns the first reference; concurrent requests with one key still create a single lead.

  • A crash mid-send

    Rows stuck in sending for ten minutes are reclaimed, and the idempotency key stops a second delivery.

  • A bounce

    The email service’s signed webhook marks the row dead once, so the team sees the email never arrived.

  • Sign-in keys unavailable

    If the Access signing keys cannot be fetched, the admin API refuses every request.

Admin

An admin behind Cloudflare Access

Cloudflare Access sits in front of the admin, and the API does not take that on trust. It verifies the Access token on every request (algorithm, signing key, audience, issuer and expiry), so it stays closed even if the Access application were misconfigured, then checks the user’s role against a permission matrix that denies anything it does not list.

Five roles cover the work: owner, sales, content, analyst and support. Changes must be same-origin JSON requests, every change writes an audit row, and responses are marked no-store and noindex. The screens below show the leads inbox and the outbox as the team sees them.

Screens

The admin, on sample data

Rebuilt in HTML from the admin’s layout, on sample data: role labels instead of names, masked addresses, no real enquiry.
admin · leads and outbox — leadsSample data
  • Inbox
  • Open
  • New
  • Contacted
  • Qualified
  • Proposal
  • Won
  • Lost
  • Spam
Leads inbox, sample data
LeadServiceStatusOwnerNext actionReceived
Operations lead · o•••@example.comChat handoffAI ChatbotsNewUnassignedReply with scoping questionsToday, 09:42
Clinic manager · c•••@example.comContact pageAI Voice AgentsContactedSalesDiscovery call bookedYesterday
Founder · f•••@example.comTools planWorkflow AutomationQualifiedOwnerSend the proposal3 days ago
admin · leads and outbox — outboxSample data
Notification outbox, sample data
NotificationStatusAttemptsNext / sentLast errorAction
Team alert · ML-7K3Q9Psent2 of 6Sent 09:46—
Visitor acknowledgement · ML-7K3Q9Psent1 of 6Sent 09:42—
Handoff alert · ML-4D8W2Rfailed3 of 6Next try 10:21provider 503Replay
Team alert · ML-2H6N5Tdead6 of 6Dead-letter alert sentprovider 503Replay

Checked properties

What the tests hold it to

Properties of the code, each enforced by a test in the repository. They are not measured results.
  • Checked property

    The same submission sent twice creates one lead and one set of emails

    Checked by tests/functions/contact.test.mjs, including concurrent submits that share one key

  • Checked property

    A visitor sees a reference only after the lead is stored

    Checked by the intake test: the reference returns after the D1 batch commits, and email is sent afterwards

  • Checked property

    A failed email is retried and, after the last attempt, raised to the team

    Checked by the outbox tests: backoff, the dead state, a dead-letter alert without personal data and a lead event

  • Checked property

    No admin request reaches a handler without a verified Access token and a role

    Checked by tests/functions/admin-auth.test.mjs: expired, foreign, tampered and unsigned tokens are refused, and a key outage fails closed

Status

What runs today

As of 18 Oct 2026

  • Form checks, consent, honeypot and rate limitLiveRun on every submission today.
  • Stored leads, submission keys and the outboxPendingSwitch on when the production D1 database is attached; until then each lead is emailed to the team directly.
  • Scheduled retry workerPendingDeployed after the database; meanwhile failed rows retry on later submissions and on replay.
  • Admin behind Cloudflare AccessPendingWaits on the Access application and the owner list; until then the admin API refuses every request.

In the repository

Where it lives

Code

  • functions/api/contact.js

    The intake: checks, submission keys, the D1 batch, the no-database fallback

  • functions/api/_lib/outbox.js

    Enqueue, claim, send, backoff, dead letter

  • migrations/0001_init.sql

    The D1 schema

  • workers/outbox-cron/

    The scheduled drain and retention

  • functions/api/admin/_middleware.js

    Origin checks, Access token verification, roles

  • components/admin/

    The admin screens

Tests

  • tests/functions/contact.test.mjs

    Storage, keys, concurrency, fallbacks, retries, dead letters, the scheduled worker

  • tests/functions/admin-auth.test.mjs

    Access tokens, the role matrix, cross-site rules, masking, CSV export

Built with

  • Cloudflare Pages Functions
  • Cloudflare D1 (SQLite)
  • Workers KV rate limiting
  • Scheduled Cloudflare Worker
  • Transactional email API
  • Cloudflare Access
  • Next.js admin (static export)
  • node:test suites

Retrospective

What we would change

  1. Make storage a launch requirement

    The fallback without a database exists because this site went live before its database. For a client we would not open the form until storage is attached.

  2. Push instead of poll

    Failed rows wait for the next five-minute tick. A queue consumer would retry on its own schedule and remove the separate worker deploy.

  3. Suggest merges for repeat enquiries

    The same person writing from two tabs becomes two leads. The admin could suggest a merge by email address without doing it automatically.

FAQ

Questions about this build

Are the leads shown on this page real enquiries?

No. The pipeline is an in-house build that handles the enquiries sent to us through this site, but every row in the screens on this page is sample data with role labels and masked addresses. No real lead, name or message appears anywhere here.

Why not just email each enquiry to an inbox?

Because the email is then the only copy. Storing the lead first and sending afterwards means an outage of the mail service delays a notification instead of losing an enquiry, and the outbox shows what was sent, what failed and what is still waiting. It is the same pattern we use when we design lead capture for a client.

What does the submission key protect against?

Retries that look like new submissions: a double click, a browser resending after a dropped connection, or a visitor pressing submit again. The browser creates one key per submission. The same key again returns the original reference and sends nothing new, while a key reused for a different person is stored as a new lead and never answered with someone else’s reference.

Who can see the leads in the admin?

Only people signed in through Cloudflare Access who also hold a role. Owners and sales see every lead, support sees only leads that came from a chat handoff, and the content and analyst roles see none. List views mask personal details, every change is written to an audit log, and CSV exports neutralise spreadsheet formulas.

Could a pipeline like this sit in front of our existing CRM?

Yes. The intake, the outbox and the retries stay the same, and the delivery step writes to your CRM through its API instead of, or as well as, emailing a team inbox. A CRM write is treated like an email: it is queued, retried with backoff and raised to a person if it keeps failing.

Worked examples

Concept builds for this service

Concept builds are worked examples, not client work.

See all concept builds
Concept build

Workflow Automation

Booking, Reminder and Intake Flows for a Multi-doctor Clinic

A concept build showing how we would automate the admin around each appointment at a multi-doctor clinic: booking, reminders, intake, follow-up and reporting. It would sit on top of the clinic management software the team already uses, carry appointment logistics only and leave every clinical decision with the clinic.

  • Healthcare
  • Sep 2026
Concept build

Workflow Automation

Invoice Intake and Approval Workflow for a UK Finance Team

A concept build for the finance team of a UK small or mid-sized business that keys supplier invoices into its cloud accounting system by hand. Invoices arriving by email are read into structured fields with a confidence score, matched to purchase orders, approved in team chat by the budget owner and posted to the accounting system, while anything that does not match lands in an exception queue with the reason attached.

  • Finance and accounting
  • UK market
  • Oct 2026
Concept build

Workflow Automation

New-hire Onboarding Workflow for a Growing Startup

A concept build for an Indian startup that hires every month and runs onboarding from one person’s memory and a long checklist. A signed offer starts a workflow that creates the email and team-chat accounts, raises the laptop ticket, shares a day-1 checklist and books the 30-day check-in. When a step fails, it retries and tells that step’s owner instead of failing silently.

  • Startups
  • India market
  • Oct 2026
Concept build

Workflow Automation

Claim Intake Triage for an Insurance Broker

A concept build for an independent insurance agency in the US, the broker between its policyholders and several carriers, that receives first notices of loss by email and web form with photos and documents attached. Intake reads each claim into a structured record, checks it against a completeness checklist for that line of business, asks the policyholder for anything missing right away, and queues complete files for an adjuster. The AI never decides coverage.

  • Insurance
  • US market
  • Oct 2026

Next step

Have a workflow in mind? Let’s scope it together.

Book a free 30-minute strategy call. Bring one process you want to automate and we will talk through scope, integrations and a sensible first step.

Free 30-minute call · no commitment

Send an RFP or security questionnaire