Skip to content
← all notes

dlvx notes · no. 003 · 9 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.

how to brief a property website-to-crm integration

If you're planning this for a property agency in Thailand, bring one enquiry to the first meeting. Pick one your team had to untangle. Remove the customer's personal details, then show where it arrived, which property it referred to and who was supposed to act.

Here is an example of what I would ask a supplier to demonstrate. It is a proposed test, not another list of ProPick features.

A renter asks to view a condo on Saturday afternoon. They submit the form twice, then ask about a second property. Walk that through the system:

  1. Keep the original request and link it to the correct listing reference. The second property needs its own link to the enquiry history.
  2. Match the contact using agreed rules. A name alone is not enough. A repeated submission should not silently overwrite the earlier request.
  3. Record the source where it is available. If the source is unknown, leave it unknown.
  4. Assign a person to act. Show where the request goes if the usual agent has not been assigned or cannot take it.
  5. Keep Saturday afternoon as a requested time until someone confirms the viewing.
  6. Disconnect the CRM and try again. Show who sees the failure and how the transfer is retried without creating another enquiry.

Then settle the questions that will otherwise come back during the build:

  • Which record owns availability and price? What happens when two people change it?
  • Do Thai and English descriptions share one listing ID?
  • Who can see, change and export customer records?
  • Which outside services receive the data?
  • Who owns the connection settings and documentation?
  • Who fixes a failed connection after launch?

For AI-assisted work, keep the source message beside the output. A summary should preserve the customer's stated preferences. A budget they never gave should stay blank. Test a message that mixes Thai and English, and one where the customer changes their mind.

Use a rule when the rule is already clear. If the listing names its responsible agent, the system has enough information to assign that enquiry without asking a model to guess.

Agree the tests before the build. Count failed transfers, duplicate enquiries and time to ownership before and after release. Those are useful operational checks. Sales and revenue need their own evidence.

You can bring that workflow to DLVX with a list of your current tools and the point where the handover breaks.

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.

Want the same, or want to know if it applies to you?

Tell us the situation in two lines. We answer personally, usually the same day, and we say so if it is not a fit.