Madmaxlabs

We replaced a 30,000 rupee a month CRM with 469 lines of Node

How a lead gets from a form on this site to Falcon's phone with no CRM, no email service and no account anywhere. What broke first, the two rules we kept, and what it cost.

Short version: every form on madmaxlabs.com now writes to our own database first, then the site itself pushes a notification to Falcon's phone and desktop. No CRM, no email provider, no account at a third party. The transport is 281 lines, the fan-out is 188, and the whole feature added no packages. This is the long version, because the mistakes on the way are the useful part.

What we were paying for

Until August we ran the site's lead capture through GoHighLevel, the same platform we used to schedule social posts and to run a white-label experiment. It cost about 30,000 rupees a month. For the site itself, the job it did was small: when someone filled in a form, the submission landed in its contact list and we saw it there.

We cancelled it on 4 August. That afternoon every form on the site broke in a way nobody noticed. The form still posted to our server, our server still tried to hand the lead to the cancelled CRM, the CRM answered 401, and our code turned that into an error for the visitor: "Could not save your details. Please try again." The submission was not written anywhere. The only trace of a lost lead was a bare email address in a rate-limit table, with no name and no message.

That is the first lesson, and it has nothing to do with CRMs. Ask where a form on your site sends its data, and what happens to a submission when that place is down. If the answer is "it fails", you have been losing leads on every outage you never heard about.

The two rules we kept

We rebuilt the pipeline the same week around two rules that are now baked into the code.

Store first, notify second. Our own database is the system of record. A submission is written there before anything else happens, and only that write can fail the request. Everything downstream, the alert and the optional CRM push, runs after the visitor already has their confirmation and cannot change what they see.

Never fake success. The old code had a branch that returned "ok" when the CRM was not configured. That is how an outage stays invisible for weeks. Now the route either stores the lead, or logs the whole payload and tells the visitor to email us instead. It never says yes and means no.

Those two rules fixed the losing-leads problem in August. They did not fix the seeing-leads problem. For the next seven weeks the only place a lead could surface was a private inbox page on the site, and between 13 August and 20 September nobody opened it. It held three rows, all our own tests. Zero real leads, and no way to know if a real one had arrived.

Notify, without a service

The obvious fix was a transactional email service, and we had one picked and half configured. Falcon's call was to stop adding dependencies: he would rather the site checked for leads itself, told him when one came in, and left the reply to him.

So the site tells him itself. The channel is Web Push, the thing browsers use for site notifications. It sounds like it needs a platform, and it does not. Here is what happens:

  1. Falcon opens the private inbox page on his phone once and taps "Enable alerts". His browser creates a subscription, which is an address at Google's push service (Mozilla's for Firefox) plus two small keys, and hands it to our site. We store that row.
  2. When a lead is stored, our server encrypts a short note for that device using its keys, signs it with a key pair we minted ourselves, and posts it to that address.
  3. The push service wakes the phone's browser, which runs a 55-line worker we wrote and shows the notification. Nobody but that browser can read the note. The push service only ever sees ciphertext.
  4. He taps it, the inbox opens, and he replies by email himself.
  1. 1
    The form

    Every form on the site posts to one endpoint. The honeypot, the validation and the rate limit run first.

  2. 2
    Our database

    The lead is written to our own table. This is the only step that can fail the request, and the visitor gets their confirmation the moment it succeeds.

    The visitor is done here. Nothing below this line can fail the request.
  3. 3
    Encrypt and sign

    After the response has gone out, the server encrypts a short note for each enrolled device with that device's own keys and signs it with the key pair we minted.

  4. 4
    The push service

    Google's for Chrome, Mozilla's for Firefox. It sees an address and ciphertext, nothing else, and wakes the device.

  5. 5
    The phone

    The browser runs our 55-line worker, which shows the notification. A tap opens the inbox and the reply is written by hand.

The path of one lead, from the form to the phone. Store first, notify second.

Three standards cover the whole thing, and each is a few dozen lines of code: RFC 8030 for the delivery, RFC 8291 for the encryption, RFC 8292 for the signature. The usual way to do this in Node is a library called web-push. We wrote it against node:crypto instead. The site already had a rule of no casual dependencies, and the specification is short enough to hold in your head.

How we know it works

Writing your own encryption is the kind of decision that deserves a receipt. RFC 8291 ships with a worked example in its appendix: a fixed set of keys and a fixed message, with every intermediate byte printed. We feed that exact example through our code on every change, and it has to reproduce the appendix byte for byte or the check fails loudly.

PASS  RFC 8291 Appendix A vector reproduced byte for byte
PASS  VAPID token signs and verifies (ES256, raw r||s)

There is also a "Send a test" button on the inbox page that pushes through the exact path a real lead takes. If the test lands, the real alert will.

What the phone shows

The title is "New lead:" followed by the name, or the email if they left no name. The body is which form it came from, what they said they need, and the first 180 characters of their message, which is about two lines on Android. Tapping it opens the inbox. If the same lead is pushed twice, the second replaces the first instead of stacking. If a device has dropped its subscription, the push service says so and we delete that row on the spot.

What it cost

Nothing on the bill. The database is the one the site already runs on. The push services belong to the browsers. The only secrets are the signing key pair, which took one command to mint.

In lines: 281 for the transport and 188 for the fan-out to every enrolled device. With the enrol button, the worker, the database migration and the two scripts that mint keys and run the self-test, the whole feature is 1,124 lines, and zero new packages.

When you should not do this

This is right for a small studio where two people read every lead and reply by hand. It is the wrong choice if:

  • More than a couple of people need to see the same inbox, hand leads to each other, or track who replied. That is what a CRM is for, and a cheap one is worth it.
  • You want automated follow-up sequences. We do not, on purpose, but plenty of stores do.
  • Your team lives on iPhones and will not add the page to the Home Screen. Safari on iOS only delivers web push to sites installed that way.

Two smaller things to know. If someone taps "Block" on the browser's permission prompt, the page cannot ask again, and they have to allow it in the browser's settings. And if you ever rotate the signing keys, every device has to enrol again.

If you run a store

You probably do not need to write any of this. You do need the two rules. Ask whoever built your site, or the agency running it, where a form submission is stored before it goes anywhere else, what a visitor sees if the CRM or the email service is down, and how you would find out. If the answers are "straight into the CRM", "an error" and "we would not", you have the same hole we had for seven weeks. It costs nothing to close.