All case studies

Web Development case study

SEO Dashboard

Built for a client. A multi-project SEO platform that replaced a workbook, with an agency console, a read-only client portal and a parity check proving the numbers match.

SEO Dashboard interface
The Problem

What was actually broken

A client ran their entire SEO practice out of one workbook. Keywords, ranked positions month by month, backlinks and their anchor text, on-page work, Search Console figures, deliverables, and a per-specialist performance table. It worked, which is the hard part to appreciate: nobody replaces a spreadsheet that is failing, they replace one that has outgrown itself.

What it could not do was let a client see their own numbers. Reporting meant exporting a slice, cleaning it, and sending a file, which is a person's afternoon every month and a version of the truth that starts drifting the moment it is sent.

The real obstacle was not building the app. It was that nobody sensible migrates off a spreadsheet they trust into a database they do not. If the new dashboard shows a keyword at position nine and the sheet says eleven, the project is over. Every figure had to be provably identical, and provably had to mean something better than us saying so.

Our Approach

The decision that shaped everything else

Two audiences, two applications, one database

The agency console holds everything: adding keywords and pages, recording monthly ranks, logging backlinks, assigning deliverables, tracking team performance. The client portal is read-only, scoped to that client's own project, written in plain language, with a message thread to their team. Same data, two completely different products, because an agency screen shown to a client is either overwhelming or has to be censored on the fly.

A parity check that reads the workbook independently

Every figure the app computes is compared against the workbook's own formulas, re-implemented separately: 16,855 checks across every field, every rank cell and every derived total, including the per-specialist table compared against the totals Excel itself had stored. That number is the argument for switching. It is not a claim that the migration went well, it is a check anyone can re-run.

The check that agreed with itself

An earlier version of that parity script reused the importer's own row filters. So it compared the importer's interpretation of the sheet against the importer's output, and agreed by construction. It reported 16,723 of 16,723 passing while fourteen real backlinks were missing from the database. A parity check that shares the importer's assumptions cannot detect the importer's mistakes, which is obvious in hindsight and was not obvious at the time.

Three places the app deliberately disagrees with the sheet

Excel's COUNTIFS ignores case, and the workbook depended on that: matching anchors exactly was silently dropping 491 links from keyword progress, so the app matches the way Excel does, with expression indexes keeping the join fast. The sheet counted a backlink twice when the same keyword sat in both anchor slots, and one link is one link. And its rank distribution pointed at a hard-coded column, so every figure now follows the selected month instead. Each difference is printed in the parity output rather than buried, because an unexplained discrepancy is what destroys confidence in a migration.

The Build

What we shipped, and what fought back

Next.js 16, Drizzle and Neon Postgres

The App Router serves both the console and the portal. Drizzle models projects and members, keywords, pages, rankings, backlinks, tasks, analytics snapshots, an activity log, and the threaded messaging between agency and client. Sessions are signed with jose, passwords hashed with bcrypt, and access is scoped per project rather than per role alone, so a specialist on one project cannot read another.

Importing the workbook rather than retyping it

The sheet is read directly, parsed and validated with zod before anything is written. Import is re-runnable and detects what is already there, which matters because the first import is never the last one: the client kept using the workbook while we built, and the migration had to absorb that.

Charts that answer the question being asked

Rank distribution over the selected month, keyword movement with the delta rather than just the current position, and backlink progress per keyword. Recharts on the client with SWR handling fetching and revalidation, so a console left open through a working day does not quietly go stale.

The Result

The numbers it holds

The workbook has a successor that two audiences use for different things, and the client can open their own numbers without waiting for someone to export them.

The part worth keeping is the parity check, and specifically the version of it that was wrong. It passed, it looked rigorous, and it was measuring the importer against itself. We now write verification against the original source rather than against our own interpretation of it, on every migration we take on. A check that cannot fail is not a check.

Built with
Next.js 16TypeScriptDrizzle ORMNeon PostgresTailwind CSSRechartsSWR

Replacing a spreadsheet that a business already trusts is a category of work we take on often. It sits under our Next.js web development service, where the scope, timeline and starting price are published before you talk to anyone.

Next case study

Clora AI

Tell us what’s slowing you down.

Fixed scope, quoted before kickoff, four to eight weeks to production. You talk to the engineer who writes your code.

Start a project