Technical Due Diligence Checklist Before Buying Software

A technical due diligence checklist for founders and investors who need to prove a codebase, its data, access and recovery story are real before a deal closes.

I blocked the deletion of a source platform after a migration appeared finished, although the database had properties, inspections, report bodies, PDFs, meter readings and media, and the import jobs were green, so a less careful review could have deleted the old account; for a UK property inventory agency, I built a per-property manifest from read-only production SQL and live source API reads, separating report content from image backup because a property can look complete in a table while its evidence is missing from storage, and cover images and meter photographs without UUIDs had silently bypassed the earlier download path, so we backfilled and verified them, but deletion remained blocked while a separate actions backfill was incomplete.

That is useful technical due diligence: establish what is true before you buy a company, invest, or agree to own the problems after someone leaves, because a founder or investor does not need every line of code, only checkable answers, dated evidence and a view of unproven claims.

Technical due diligence begins with data you can count

Does the application reconcile with the place its data came from, since “Did someone run an import?” and “are the screens populated?” are not proof.

Ask the seller to identify every source of record - production database, old application, Stripe, CRM, accounting package, data warehouse, object storage, operations spreadsheet or a third-party platform being replaced - then record for each what should have arrived, what was deliberately excluded and who accepted that exclusion.

Reconcile by domain, not only total rows: compare properties by source identifier, then their inspections; invoices by identifier and financial totals by period; and attachments as well as database rows, because a main record can exist while the thing that makes it useful does not.

The property-platform manifest was per property, not one comforting aggregate, with each row carrying separate content and image-backup statuses because report-body snapshots, terminal-report PDFs, meter-reading text and images had different acquisition paths and failure modes, which one green count would have hidden.

For the checked set, 10,195 properties were content-green, 61 content-amber and none content-red; amber meant an explicit gap needing follow-up, not a pretend binary answer, so it told a buyer what they were accepting.

Ask for the manifest, not a slide derived from it: it needs stable source and local IDs, checks and their states, failure explanations and run time, and you should be able to sample ten rows and trace each result to a source response or stored snapshot, because without this for important data, completeness is an assertion.

There are a few questions I would ask in every review:

  • Which source IDs exist in the source but not locally, and why?
  • Which local records have no source counterpart, and are they legitimate local additions or duplicates?
  • For child records, which parents have fewer children than the source?
  • Which files, generated documents, or images were not copied?
  • Can the check distinguish unavailable data from data never requested?
  • What date and source version does this comparison represent?

“How many records did you migrate?” invites a round number, while “Show me the source IDs missing from the local database” invites a fact.

The first image audit found 14,179 cover-image URLs and 42,303 meter-photo URLs absent from local storage, although the records looked fine until someone needed the evidence, and the missing files had no UUID, so the earlier path considered only media with one; after the fix, a second run found all 17,595 distinct cover-image URLs and all 42,494 distinct meter-photo URLs in imported storage, and both counts should be preserved because keeping only the second removes the decision record.

An API comparison must not mutate production, so use read-only credentials and SQL where possible, record inputs, result, errors and pagination state, and reject a run that updates timestamps, unexpectedly refreshes tokens or triggers webhooks because it has changed what it measured.

If a seller says the old source can be deleted, ask for the condition authorising it, because “The team is comfortable” is not one, and in this migration deletion stayed blocked until photographs were present and the remaining actions backfill completed, so the schedule was less attractive but the archive was believable.

A source snapshot makes a claim inspectable later

Reconciliation is stronger when the application preserves what it received: on this platform, each fetched payload was stored as a local snapshot before projection into canonical records, so operational tables served the product while snapshots could later show what the source returned at import time, but transformations that strip fields, change formats, combine records or download from expiring URLs need enough provenance to explain a local record, rather than every byte forever, because retention, privacy and cost matter.

Pick a meaningful record yourself - customer account, paid invoice, report, employee record or privacy-sensitive document - and ask the seller to show its source ID, first import, later changes, actor or integration for each change, and file location, because a half-hour Slack search is key-person risk; an audit trail should record understandable domain actions - who changed bank details, approved a refund, delivered an integration update or replayed a failed event - alongside relevant previous and new state, and for destructive actions, whether and how they can be reversed.

An audit trail must be difficult to edit casually and cover business risks, including account deletion, permission changes, exports, billing, moderation and automated integration writes, because updated_at is not an audit record, so separate append-only events are better; permissions, immutability controls and retention help, but ask whether an administrator can quietly rewrite history, especially on a platform handling children or sensitive feedback, which needs tighter access and retention controls.

I saw this on a youth education platform with educators, volunteers, administrator roles and anonymous student feedback, where the policy covered GDPR processing, rights requests, retention and anonymous feedback, public student links were signed for six months, and the repository also had an empty safeguarding document and an external-policy hand-off, so record that plainly: a policy obligation exists, but an in-application incident process was not evidenced.

The trail must be retrievable, so ask someone other than its author “Which staff members exported this account's data in the past ninety days?”, “What changed this subscription to cancelled?”, and “Which webhook retries created this duplicate?”, because if they must correlate three log services by hand, it fails during an incident.

Test coverage is a map of known risks, not a percentage

Coverage alone tells a buyer little, because high line coverage can miss an integration, deploy script, worker, permissions boundary or old migration, while low coverage can coexist with well-covered financial and identity paths, so ask what CI runs and for test names around login and role changes, payments and refunds, tenant boundaries, deletion, export, webhooks, imports, scheduled jobs, deployment migrations and failed third-party calls.

On the property platform, useful tests covered paged root selection, persisted cursor resumption, a failed root not advancing the cursor, retry accounting after a 429, optional source endpoints failing, idempotent upserts and an old import not overwriting a newer webhook update, which shows operational intent, but they still did not justify deleting a source, because tests describe controlled inputs and future edits while the read-only integrity run showed whether imported production data matched the checked source set closely enough for a deletion decision, so you need both.

Compare declared behaviour with tests: does a README promise tenant isolation while tests only assert an unauthenticated redirect, are retries “safe” without proof that a job can run twice without duplicating a charge, and does a million-record migration lack a test or dry run, while a small relevant sample beats a large count, so you should check when the suite ran and against which database and services, because a run from six months ago, or one faking every provider boundary, does not prove OAuth scope, webhook signature, rate limit or storage permission works in the account operating after the deal.

For legal or safeguarding surfaces, also inspect consent text, retention settings, access model, subject-access process and incident path, because “GDPR compliant” means nothing without the underlying decisions, documents and responsible owner.

A backup is only real after a restore

Every due-diligence folder mentions backups, but few contain a dated restore record, so ask where backups live, retention, encryption, account ownership and whether application data and files are covered together, because a database backup without object storage restores rows pointing nowhere, while storage without the database leaves opaque files without relationships; configuration, encryption keys and secrets matter too, and a backup that cannot be decrypted has not recovered.

Ask for the most recent restore - environment, date, operator, inputs, elapsed time, errors and checks - because “We could restore if needed” is not evidence, whereas “We restored a copy to an isolated environment on 12 May, checked row counts, logged in as a test user, opened a private document, and discarded the environment” is.

State recovery goals in business terms, because acceptable data loss and downtime determine backup frequency, replication and cost, then restore a representative record with relationships and files to see whether a user can sign in, render it, download its attachment and run a background job without reaching production; a no is a recovery gap before an outage, and the exercise must stay isolated because a casually accessible staging restore creates a privacy problem, especially for children or anonymous feedback, so it needs access controls, a retention rule and a way to stop test emails, payment calls and outbound webhooks.

Credentials and vendors decide who can operate on Monday

The company may own the software while a former employee's personal account owns its operation, so make an access register for the repository, cloud provider, domain registrar, DNS, email and error-reporting providers, database host, object storage, payment processor, analytics, support tools, relevant app-store accounts and core integrations, recording legal owner, administrators, billing contact, recovery email, MFA, API-key location, renewal date and whether the buyer's nominated operator has tested access.

A dashboard screenshot is not control: have the owner add a nominated person, complete MFA and prove they can operate without a code from the seller's phone, then remove access only after contractual handover conditions are met, while including third-party limits because they become constraints in a migration, reindex, backfill or sudden growth event.

The property platform's source API had an approximate shared ceiling of 60 requests a minute, and because daily operations used the account, historical work was held to 40 requests a minute; the import honoured Retry-After for 429s, retried transient 5xx failures up to four times and kept heavyweight media work away from webhook-critical queues, which says what happens when the vendor says no and prevents backfills disrupting the business.

Get material-provider rate limits in writing, then ask whether a token or quota is shared with live work, what a week of 429s does to a queue, whether retries are bounded and reach a human, and whether jobs resume from a stable cursor, because “We have retries” answers none of this.

Check OAuth: who holds client credentials, where refresh tokens live, what revocation does and whether two processes can refresh at once, since a read-only live verification run once had two processes fight over a refresh token and failed before writes, so the rule became to use the current access token without a competing refresh, while such rules must become operating facts, not one person's memory.

A deploy story needs a route back, not a brave person

Ask about the last production deploy, not the ideal process: you need the deployed revision, approver, configuration source, database changes, pre-traffic checks, logs and errors, and route back to the prior revision, because shell history on a laptop is material risk, while destructive database migrations may not roll back after new writes, so a credible story identifies migrations safe before the code change, those needing staged release, feature flags or temporary compatibility, and when rollback stops being simple.

The education platform had configured Docker and Kamal deployment backed by PostgreSQL, with background work in the web process, which showed intended shipping rather than a production deploy, experienced operator or tested rollback, because configuration is evidence of intention, while a dated release record is evidence of operation; ask for a non-production rehearsal that deploys the current revision, smoke-checks it, deploys a deliberately broken but harmless revision, rolls it back and records the time, because if the seller cannot do that before close, price the uncertainty as risk.

Good diligence needs limits that are visible, measured and owned, so the deletion gate, image gap, restored backup, access register and tested rollback ask one question: when the seller says this works, what can they show you?

I build and audit systems that need to survive handover, not just ship a demo; for the work behind this checklist, my CV and rates are here, and if you are estimating a replacement rather than buying one, read what actually drives MVP cost.

Related