← all notes

dlvx notes · no. 003 · 6 min read · 2026-09-05

a website is not a lead system: propick had six live pages and 123 rows in a spreadsheet

ProPick's old site looked fine and produced almost nothing, because the inventory lived in a Google Sheet and lead ownership lived in one column. Here is what the rebuild actually was, what it cost, what broke after I shipped it, and how to tell whether you need a website or an operations layer.

On ProPick's old setup, ownership of a lead record lived in a single column. If an admin created the lead, the agent who was supposed to call that person never saw it on their dashboard.

Nothing was broken in the way anyone checks for. The form submitted. The record was created. It was created where the person who needed to act on it could not look.

That failure mode is invisible from the outside. You cannot see it in a screenshot and you cannot fix it with a redesign. It is why I keep saying the website is usually the smallest part of the job, and it is why I want to write down what the ProPick build actually was, with the numbers, including the parts that went wrong after I shipped.

the site was six pages, the business was 123 rows

The before state: WordPress, a bought theme, eight plugins, six or seven live property pages. The inventory, which is the thing the business actually sells, lived in a Google Sheet with 123 properties in it.

So the visible business was six or seven pages and the real business was 123 rows that no buyer could search, filter, or land on from Google. A better theme on top of that changes the first impression and nothing else. The ceiling was not design. The ceiling was that the inventory was not in a system.

This gets sold the wrong way round constantly, and I understand why. A site redesign is scoped, sellable and photogenic. The client agrees to it because the site is the part they look at every day. The inventory problem is none of those things, and it is the actual job.

three layers, and one of them is a website

What got built was three layers.

The public site: 35 routes, 483 listings live in public search, 5,239 photos processed. The CRM: 14 dashboard routes, 557 properties, and 500 reserved reference codes migrated so the agency's own numbering survived the move. The gap between 557 and 483 is stock held back from publication, which is most of the reason to have a system at all.

Then the layer nobody asks for: 41 row-level security policies, 31 on tables and 10 on storage. That is the layer deciding who can see which lead, which is precisely what was broken before. The whole thing is 32,980 lines across 82 commits.

The warm homepage answers in 43ms. I will put that here because people like the number, and then say that it is the least interesting one in this piece. A fast homepage on top of a spreadsheet is a fast brochure.

the numbers that matter are not site numbers

267 real leads captured, 87 of them in the final 30 days. 49 viewings booked, 28 completed. 12 won deals. 1,081 agent activities logged.

Those five numbers are the deliverable. Not the page count, not the load time. A lead is an unverified claim until someone stands in the apartment, so 49 booked and 28 completed tells me more than 267 captured does. The 21 that did not complete are now a visible number somebody can chase. Before, they were not a number at all, because there was nowhere for them to be one.

The 1,081 logged activities are the quiet one. That is the record of the business running: who touched which lead, when, and what happened after. Without it, an owner is managing from memory and from what people tell them, which is close enough to not managing.

the search growth was not clever seo

Impressions grew 15.9x over the window the case study covers, and average position moved from 15.9 to 13.5. The most recent 28 days: 3,955 impressions, up 3.4x, and 79 clicks, up 2x, across 154 pages and 169 queries.

The cause is unglamorous. The site went from six or seven property pages to 483 published listings. Nothing was optimised in the sense a consultant means it. A spreadsheet was moved into a database, and the database was allowed to render pages that persist at their own URLs.

I think that is the mechanism behind most search growth for a business with real inventory. Not a content plan, not a keyword map. If a business has 123 sellable things and 6 pages, the growth work is a data problem wearing a marketing hat.

the failures

The system shipped, and then it lied to me, which is worse than breaking.

The lead health check was green for four days and proved nothing. The probe suppressed its own notification send by matching on its own synthetic tag, and the suppression caught real traffic too, so two real submissions from real users went completely unnotified while the monitor stayed green. The fix is not suppressing better. It is overriding the notification recipient to an internal address and asserting the send actually succeeded, by capturing the provider's message id, instead of asserting that a row landed in a table.

Underneath that sat my own design mistake. The notification send was fire-and-forget inside a deferred callback with errors swallowed into a console log. That can stop firing for weeks with no symptom anywhere. Now the send is awaited, the message id or the exact error comes back in the API response, and a notified_at timestamp is stamped on success, so a later query can find exactly what was never sent. The rule I kept: a notification failure must never fail the form submission underneath it.

Two more from the same family. Filtering the leads table on an assumed status value returned zero rows and looked like a perfectly worked queue, because the real label in use was a different word entirely. Group by the status column and read the distinct values before trusting any count against it. And the anonymous public role held a table-level SELECT grant, so a plain select of everything returned owner names, phone numbers and messaging IDs across hundreds of rows. A column-level REVOKE is a no-op against a table-level GRANT in Postgres. The only thing that works is revoking the table grant, then granting an explicit column list back.

There is also the deploy trap that produced my favourite sentence about this industry. A deployment can return HTTP 200 with every image on the page broken, because next/image routes through a metered optimizer that returns 402 once the quota is gone, so the tags render and every image is zero pixels wide. Green is not correct. Green is only green.

which one you need

Three questions.

Where does the inventory live. If the honest answer is a spreadsheet, a shared drive, or one person's head, a new site publishes a fraction of the business and hides the rest.

What happens to an enquiry at 9pm on a Sunday. If it is seen tomorrow, when someone remembers to check, that is a form, not a lead system.

Can you say how many viewings or calls happened last month without asking anyone. If not, nothing built on top of it is measurable, including the site.

Clean answers to all three and you probably do just need a better website, and you should buy one. Fail any of them and the site is the last thing to fix.

what my own site looked like

ProPick paid 200,000 THB flat, in four instalments of 50,000, with support afterwards at 2,500 THB an hour and no retainer. I price it that way because a retainer pays me for the system staying complicated.

And then this week I audited dlvx.co. My own site published six different founding years: 2017 across five files, 2021 across nineteen, 2023 across five, 2024 across seventeen, and one file each for 2025 and 2026. One translations file carried two of them by itself. Every Thai Person node said it worked for an organisation URL that no page on the site ever declared. Every service page named its provider as piexels.dev, a domain that is dead. The Thai services hub canonicalised to the English one, so 6,820 Thai characters could never be indexed. The favicon beside every Google result read "iexels", because the file being served was the old Piexels wordmark cropped square.

Ninety days of Search Console for dlvx.co: 23 clicks, 1,687 impressions. ProPick did 79 clicks and 3,955 impressions in 28 days.

I had built the studio a brochure and sold everyone else a system.

I build brand, software and the ai layer behind both, from bangkok. every price is on the pricing page, and you own the repository from the first commit.

start a project