Skip to main content

How to Estimate Engineering Hours for a Directory MVP (With a Template That Survives Contact)

A three-point estimation template for a directory MVP, built from documented vendor obligations instead of feature names — plus the research on why your first number is 30–40% low and what to hand your finance team instead.

DirectoryLaunch Team13 min read
How to Estimate Engineering Hours for a Directory MVP (With a Template That Survives Contact)

The estimate you are about to write is already wrong, and you can predict the direction. A review of surveys on software effort estimation found that most projects — 60–80% of them — overrun, and that the average overrun sits at 30–40%. A separate survey of 52 Norwegian projects in the same body of work put the frequency at 76% and the average magnitude at 41%. That research is two decades old, which is precisely what makes it useful: the same thesis notes that "estimation performance has not changed much the last 10–20 years."

So this article is not going to hand you a number for a directory MVP. It is going to hand you a method for producing one that your finance team can defend three months later, plus a filled-in template you can copy. The method has one central move: stop estimating features and start estimating obligations.

Why the feature list is the wrong unit of work

Ask three developers to estimate "Stripe payments" and you will get three numbers, all small, all confident. That is not a character flaw. The same research found something specific and uncomfortable: in a controlled experiment, professionals in technical roles — project managers and developers — "were significantly more optimistic, and less realistic, than professionals in non-technical roles (sales managers and user analysts) when estimating project effort."

The reason is that a feature name is a container with unknown contents. "Stripe payments" sounds like one integration. Read the vendor's own documentation and it decomposes into a list of separate, non-negotiable engineering commitments. From Stripe's webhooks guide alone:

  • Your endpoint "must quickly return a successful status code (2xx) before any complex logic that might cause a timeout" — so fulfilment has to move to a queue, not run inline.
  • "Stripe requires the raw body of the request to perform signature verification" — so your framework's body parser has to be bypassed on exactly that route.
  • "Webhook endpoints might occasionally receive the same event more than once" — so you need a processed-event log keyed on event ID.
  • "Stripe doesn't guarantee the delivery of events in the order that they're generated" — so your subscription state machine cannot assume customer.subscription.created arrives before invoice.paid.
  • Stripe "attempts to deliver events to your destination for up to three days with an exponential back off in live mode" — so a bug you ship on Friday keeps replaying into Monday.
  • If you use a framework with CSRF protection, "you might need to exempt the webhooks route from CSRF protection."

That is six discrete pieces of work, each with a failure mode that only shows up in production, each needing a test. None of them is visible in the phrase "Stripe payments." An estimate built on feature names systematically omits them, which is one concrete mechanism behind the 30–40%.

Build an obligation inventory instead

The unit of estimation that works is the documented obligation: a requirement written down by a third party you have chosen to depend on, which you cannot argue with and cannot descope. Vendor docs, platform policies, and legal terms are all sources. They are also verifiable, which means your estimate stops being an opinion and starts being a reading of published constraints.

Three that catch directory teams repeatedly:

Your database is open by default. Supabase's own security guide states that on existing projects, "tables created in public receive SELECT, INSERT, UPDATE, and DELETE privileges for anon, authenticated, and service_role by default," and that "these grants make new objects reachable through the Data API, even when you don't intend to expose them." The instruction is to enable RLS on every table and view exposed through the Data API, and tables created in the SQL Editor or via migrations need it enabled explicitly. A directory with a dozen tables and two roles is not one RLS ticket. It is a policy per table per operation, plus tests that assert both allow and deny.

Your map has a meter on it. Google Maps Platform bills per SKU with a free monthly cap per SKU. Per the core services price list (page last updated 2026-09-01), Autocomplete Requests have a 10,000-event free cap and then cost $2.83 per 1,000 up to 100,000; Places API Place Details Essentials and Geocoding are $5.00 per 1,000 on the same 10,000 free cap; Dynamic Maps loads are $7.00 per 1,000. Google's own worked example on that page: 200,000 Autocomplete billable events in a month costs $481.70. That turns "add a map" into caching, session tokens, field masks, and a debounced autocomplete — engineering work whose purpose is to keep an invoice down.

Your free hosting tier may not apply to you. Vercel's Hobby plan documentation restricts Hobby teams to "non-commercial personal use only" and defines commercial usage as "any Deployment that is used for the purpose of financial gain of anyone involved in any part of the production of the project, including a paid employee or consultant writing the code." Read literally, paying a contractor to build your directory makes the deployment commercial before the site takes a single dollar. That is a line item, not a surprise.

The two-column test

For every row in your estimate, write the obligation in one column and the URL that states it in the other. Any row where the second column is empty is not an estimate — it's a guess wearing an estimate's clothes. Guesses are allowed; they just have to be labelled, because they're where the variance lives.

The template: three points per block

Once you have obligations grouped into blocks, estimate each block three times — optimistic, most likely, pessimistic — and combine them with the PERT weighting (O + 4M + P) / 6. Three points are worth the extra minutes because they force you to name the pessimistic case out loud, which is the number a single-point estimate quietly deletes.

Here is a filled-in inventory for a directory MVP with listings, owner claims, paid placements, maps, and programmatic location pages. The hours assume a team that already knows Next.js, TypeScript, and Postgres; they are illustrative and meant to be overwritten, not adopted.

BlockOMPPERTSpread (P−O)/6
Listing model, CRUD, moderation states24408044.09.3
Search, filters, pagination20367239.38.7
Auth, roles, RLS policies and allow/deny tests24449649.312.0
Claim flow and owner verification20368040.710.0
Checkout, webhooks, subscription state machine326012065.314.7
Maps, geocoding, response caching12245627.37.3
Programmatic location pages and sitemaps16327236.09.3
Admin panel and moderation queue244810052.712.7
Transactional email, deliverability, anti-spam12245627.37.3
CSV import and data cleanup12286431.38.7
Total196372796413.2

Check the arithmetic on one row so you trust the rest: for payments, (32 + 4×60 + 120) / 6 = 392 / 6 = 65.3.

The part where the maths lies to you

Combine those spreads the textbook way — square them, sum them, take the square root — and you get a standard deviation of about 32 hours on a 413-hour project. Roughly 8%. If you present that, you are telling a finance team you can hit 413 hours give or take a week and a half.

You cannot, and the reason is that the textbook method assumes the blocks are independent. They are not. If your auth and RLS work slips, the claim flow slips behind it, and the admin moderation screens slip behind that, because they all read the same permission model. Correlated risk does not cancel out — it stacks.

So use the arithmetic for relative sizing and use the empirical record for the buffer. The research says the average overrun is 30–40%, and 41% in the Norwegian survey. Applied to 413 hours that gives roughly 537–583 hours as the honest planning range.

Don't bury the buffer

Contingency hidden inside each block gets spent inside each block, silently. Contingency shown as its own line — "PERT total 413h, empirical overrun allowance 30–41%, planning range 537–583h" — survives review, because it names the reason instead of padding the rows.

Two more findings from the same body of research are worth acting on, because both are cheap. First, combining expert estimates through unstructured group discussion "can reduce the existing bias towards estimates that are overly optimistic" — so estimate as a group, not as a spreadsheet owner. Second, flexible development models (incremental, iterative, agile) showed a lesser magnitude of overrun than the sequential waterfall model. If your directory ships category-by-category rather than in one launch, your estimate gets better as you go instead of only getting worse.

Turn a feature list into an obligation inventory

The mechanical part of this — reading vendor docs and extracting the commitments hiding inside a feature name — is exactly what a language model is good at, provided you make it cite the source and refuse to invent.

Then do the thing that makes it real: open every URL it produced and read the quote in context. The prompt is a search tool, not an authority.

Where directory estimates specifically go wrong

Beyond the generic causes, four line items are consistently under-scoped in this product category.

Owner verification is a product, not a form. Handing a listing to the person who claims to own the business means deciding what proof you accept and what you do when two people claim the same listing. We broke that flow down separately in listing claims and owner verification; the short version is that the code is easy and the policy decisions are not, and policy decisions cost calendar time rather than hours.

Programmatic pages are not free traffic. Generating 5,000 city-and-category URLs takes an afternoon. Getting them into an index does not, and the failure is silent — the pages exist, they just never rank. We covered what actually causes that in discovered — currently not indexed. Budget the diagnostic work, not just the generation.

Imported data is dirty data. A CSV of 2,000 businesses will contain duplicates, dead URLs, inconsistent categories, and addresses that do not geocode. The import script is the small part. Reconciliation is the rest, and it recurs every time you import.

Someone has to run it. Moderation, refunds, support email, and dependency updates are not in the build estimate at all, which is fine — as long as your document says so explicitly instead of implying that launch is the end of spending.

When the estimate is the wrong artefact

If your inventory lands anywhere near 400 hours, the honest next question is not "how do we get it to 300?" It is "should we be writing this at all?" A five-hundred-hour build is a real decision, and there are cases where each answer wins.

Build it yourself when the requirements are genuinely unusual — a bespoke matching algorithm, an unusual compliance regime, an existing internal platform the directory must live inside. Nothing off the shelf will bend that far, and forcing it costs more than starting clean.

Use Sharetribe if what you actually need is a marketplace where users transact with each other and the platform takes a cut. That is a different product with different obligations — payouts, escrow, dispute handling — and a directory boilerplate is the wrong starting point for it.

Use WordPress and a directory plugin if you have no developer on the team and no budget to hire one. It is slower and less pleasant to extend, but it is operable by a non-engineer, and an ugly site that someone can update beats an elegant one nobody can touch.

Start from a boilerplate when the requirements are ordinary — listings, categories, claims, paid placements, location pages — and what you want to buy is the obligations already being handled. That is what DirectoryLaunch is: the inventory above, already built, with the source included so you can change any of it. It removes build hours; it does not remove operating costs. The Maps bill is still yours, the Vercel plan is still yours if you deploy commercially, and moderation still needs a human. Our maps documentation covers the caching and field-mask behaviour so you can model the metered part before you commit.

If you want the money view rather than the hours view, our cost breakdown for a directory website puts blended rates against these blocks across in-house, agency, contractor, and boilerplate paths.

What to do tomorrow morning

Block ninety minutes and produce one artefact: a two-tab sheet.

Tab one is the obligation inventory. One row per obligation, with a documentation URL in the source column. Start with the four vendors you have already picked and read their docs for an hour — not their marketing pages, their integration guides and their limits pages. Every "you must", "we don't guarantee", and "restricted to" is a row.

Tab two is the estimate: your blocks, three points each, the PERT column, and one clearly labelled line at the bottom that reads "empirical overrun allowance, 30–40%, source: Moløkken-Østvold & Jørgensen survey review."

Then take it to the two people who will do the work and one person who won't, and re-estimate the top five blocks out loud together. The research says the group conversation pulls the number toward reality. It costs an hour. On a project this size, that hour is the highest-leverage hour in the whole estimate.