DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on Originally published at nullstud.io

Your README Is a Lie Until Someone Else Runs It

I run a small studio, which means every project I ship ends on the same question: can the client actually keep this without me?

For a long time I assumed the answer lived in the contract. It does not. I have watched a company hold the intellectual property in the strictest legal sense and still be completely stuck, because the thing was live on a hosting account registered to somebody who had moved on.

Ownership turned out to be three separate things, and most agreements only buy one of them.

Three kinds of ownership

Legal ownership is the IP clause. Assignment on final payment, with clear boundaries around any pre-existing tooling the builder brings along and around open-source components. It is the easy layer, and it is the only one most contracts actually cover.

Custodial ownership is the repository. Not a zip of the final state. The full history, every branch, and the issue and pull-request record. History is not sentimental. It is how the next engineer finds out why a line exists before deleting it.

Operational ownership is whether you can build the software on a clean machine, deploy it, rotate a credential, and fix something at nine on a Friday night. Everything above it is theoretical until that one is true.

Engineers care about the third layer. It is also the one nobody writes into a statement of work.

The failure is almost never the code

The most common ownership failure I see has nothing to do with source files. It is that the product runs on accounts registered to the builder.

The list is longer than people expect. Cloud or hosting. Domain registrar and DNS. Apple and Google developer accounts for anything in a store. Analytics and error monitoring. Email and SMS sending. The payment processor. Every third-party API key. And for anything with a phone attached, the numbers themselves, plus the recordings and transcripts.

The rule I now enforce from day one is boring and it works: accounts are created by the client organisation, in their name, on their billing, and we get invited in. Not the other way round.

Retrofitting this is possible, but each item becomes its own small project. An app store transfer, a number port and a domain move are three different procedures with three different waiting periods, and at least one of them will need a person who has since left. I have done that cleanup. It is not technically hard, it is just slow in a way that always lands during a week when something else is on fire.

The drill that settles it

None of the above proves anything until somebody tries it. So here is the test, and it is the single most useful hour I spend on any project.

Take a person who did not build the software. Another engineer, a contractor, a friendly second opinion. Give them the repository and the access list, nothing else, and no access to the original team. Ask them to:

  1. Get it running on a clean machine.
  2. Deploy it to a scratch environment.
  3. Make a trivial visible change.
  4. Ship it.

Time-box the whole thing to a day.

What comes back is never abstract. It is always a specific list, and it is usually some version of this:

  • An environment variable nobody documented.
  • A database migration that has to be run by hand.
  • A key that only exists on one laptop.
  • A build step that quietly depends on a tool version.
  • A service that was configured once in a web console and captured nowhere.

I want to be honest about something here, because it would be easy to write this as advice for other people. My own projects fail this drill too. Every time I have run it on something I built, it has found at least one item. The point of the drill is not to prove the work was clean. It is that each of those findings is cheap to fix while the people who created it are still on the project, and expensive to fix later.

Which is exactly why it belongs before final payment rather than after it.

Why the README lies

The reason this keeps happening is not laziness. It is that the setup guide is written by the person whose machine is already set up.

Every implicit dependency is invisible from inside that machine. The Node version that happens to be active. The CLI that was installed a year ago for something else. The .env that was never in the repo because of course it was not. You cannot see the gap by rereading your own README, because you are the one person on earth who does not need it.

The drill is essentially a compiler for documentation. It turns "should work" into a diff.

What I hand over now

The list I actually work through, condensed:

  • The repository, full history, all branches, issue tracker export if it lives somewhere the client can be added to.
  • A README a stranger can follow. Not a product description. The exact steps from a clean machine to a running app, including versions of anything that has to be installed first.
  • Environments and deployment, written down. If a release is a sequence of manual clicks one person knows, that is a finding, not a document.
  • Secrets in a manager the client owns, with a note on what each one is for. Credentials in a chat thread are not a handover.
  • Infrastructure as files, rather than as the shape of somebody's console.
  • The data. Schema, a documented export path, and a backup that someone on their side has actually restored once. Software can be rebuilt. A ledger cannot.
  • Source files, not exports. Design files, and for 3D or immersive work the original assets, not the optimised output.
  • A dependency and licence list, flagging anything with a commercial tier or a renewal date.
  • A short decisions note, including the compromises. Two pages of "this looks strange because of X" saves a future team a month.
  • The access list itself. Every account and service, who owns it, who is admin, what it costs. This is the document that turns a pile of logins into something a business can manage.

Where lock-in is legitimate

Not every dependency is a trap, and pretending otherwise leads to worse software. You are not going to own your telephony provider, your game engine, your cloud, or your auth service. Choosing them is normal engineering. When we build SDKs that ship inside somebody else's product, depending on the platforms those SDKs run on is the job, not a failure.

The line I use is this: you do not have to own the platform, but you must own the configuration, the data and the exit path. The account is yours. The settings are documented or exportable. Your data comes out in a usable form on demand. Somebody has thought about what a move would involve, even if nobody intends to make one.

The real lock-in is knowledge

Here is the uncomfortable part. The code was never the hard thing to transfer.

A team that has held a system for two years knows things no repository contains. Which service is flaky on Mondays. Why that retry exists. Which client email triggered the weird validation rule. That is true of an internal team as much as an external one, which means "we built it in-house" is not protection. It is the same bus factor with a different org chart.

Documentation and the drill are how you convert some of that into something transferable. Not all of it. Some of it just walks out.

None of this is adversarial and none of it is about trust. I want clients portable, because a client who could leave and does not is a much better reference than a client who cannot. It is also the only version of this business I know how to run without dreading the offboarding conversation. If it helps, the longer buyer-side version of this, including the questions worth asking before signing anything, is on the Null Studio blog.

So, a genuine question, and I am curious how many people are in the same position I was: when did you last try to run one of your own projects from a clean machine?

Top comments (4)

Collapse
 
systemcraftdev profile image
SystemCraftDev

This hits close to home. Running a one-man shop, the "clean machine" test is exactly the kind of thing I know I should do and then quietly skip because I'm the only one who ever needs to run the thing — until I'm not. The operational ownership framing is the part that'll stick with me; it's easy to hand over a repo and call it done, but a repo isn't the same as someone actually being able to keep the lights on without you. This is a good reminder that "it works on my machine" is doing a lot of quiet, invisible work.

Collapse
 
nabeelbaghoor profile image
Nabeel Hassan

The one-man shop case is the hardest one to argue with, because skipping it is genuinely rational right up until it is not. You are the only consumer, so the cost is today and the benefit is hypothetical.

What worked for me was to stop treating it as a test I have to remember to run and let CI be the second machine. A pipeline that only checks out the commit and builds, no tests, no deploy, is a clean machine running that test on every push. It turned up things I would never have thought to write down: an SDK version nobody recorded, a file that only existed locally, an env var I set months earlier and forgot about.

The ownership half you picked out is the part nothing automates though. A repo builds. Knowing what to do when it breaks at 2am is a separate handover, and it is the one that actually costs money when it is missing. Thanks for reading it that closely.

Collapse
 
systemcraftdev profile image
SystemCraftDev

That CI-as-second-machine framing is worth stealing on its own. One thing that pairs well with it if you want the same signal before you even push: git clean -ffdx in the working copy (careful, it deletes untracked files) or clone into a scratch dir and build there. Same "nothing local can save you" check CI gives you, just faster feedback and no waiting on a runner queue — good enough for catching the exact three things you listed before they ever leave your machine.

Thread Thread
 
nabeelbaghoor profile image
Nabeel Hassan

git clean -ffdx is the one I keep forgetting I own, and the scratch clone is what I actually reach for, mostly because I have nuked an untracked .env with that flag more than once. The clone catches two of the three for me: the file that only exists on my machine, and the dependency I installed globally a year ago and forgot. The one it never catches is the credential, because my shell profile is still loaded and the tokens are still exported, so the build quietly finds them and looks fine. That is the one where the second machine has to be a machine that is not mine. Fast local check first, CI as the honest second opinion, feels like the right order.