cd /news/developer-tools/build-the-simplest-thing-that-works Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-30816] src=belderbos.dev β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Build the Simplest Thing That Works

Developer Bob Belderbos built a minimal CLI CRM using two Python files, two dependencies, and Markdown files, avoiding over-engineering by constraining the design to a single user, local files, and no database. The approach emphasizes shipping the simplest thing that works and growing it based on actual needs.

read6 min views1 publishedJun 2, 2026

Shipping fast with AI but don't fully trust the code? I help developers 1:1 turn AI-built apps into something they understand and own. How it works β†’

I needed a CRM the other day. Not Salesforce, not even a web application. I just needed a way to track contacts, notes, reminders, and a small product catalog.

Before writing any code, I focused on the constraints first.

The final solution ended up being two Python files, two dependencies, and a folder of Markdown.

The Temptation to Over-Engineer #

When we developers hear "CRM," we reach for the full stack: database schema, user auth, web dashboard, API layer, deployment pipeline.

Before writing a line of code, we've committed to thousands of lines that could become tomorrow's technical debt.

Dave Thomas puts it bluntly in his book Simplicity: "Feature is marketing speak for future liability." Every feature, he argues, is code someone will have to support, maintain, extend, and understand long after you wrote it.

AI tools can scaffold a lot of code quickly. That's useful, but it also makes it easier to build far more than you actually need.

I've seen this pattern repeatedly coaching developers. It's usually justified as a learning exercise, but you learn more by shipping the smallest thing that works and growing it from there.

The Constraints That Set Me Free #

So what's the simplest thing that actually works? In Start with Design, I went from a vibe coded Rust back-end to a GitHub Actions workflow that got the job of dripping content done. Infrastructure over code.

In my CRM case, doing the scoping exercise first revealed a few important constraints:

Single user. It's my CRM. No auth, no permissions, no multi-tenancy.

CLI-first. I live in the terminal. A web UI would be overhead I don't need.

Local files. If the data is just text, I can read it, edit it, grep it, back it up, using standard Unix tools. No database means no schema, no migrations, no ORMs.

Minimal surface. The data never leaves my machine. No network calls, no third party storing my contacts.

Notice what happened here. Every constraint removed an entire class of problems:

  • Single user β†’ no auth
  • Local only β†’ no hosting
  • Files β†’ no database
  • CLI β†’ no frontend

Good architecture is often less about what you build and more about what you decide not to build.

What I Actually Built: A CLI CRM in Two Files #

Two Python files. Two dependencies: Typer for the CLI, Rich for table formatting. The repo is here.

That repo is the minimal MVP. My day-to-day CRM has since grown a few features on top of this same design. That's the whole point: start small, and let the problem pull you toward more.

Data lives in a local folder as plain Markdown:

crm/
  products.md        # Simple table: code, name, price
  reminders.md       # Due date, contact, description
  contacts/
    jd1.md           # Jane Doe (unique ID)
    bs2.md           # Bob Smith

Each contact file is structured markdown I can open in any editor:

- some metadata fields -

## Notes
- 2026-05-28 β€” ...
- 2026-05-31 β€” ...

Python for Logic, Unix for Glue #

The data folder I expose via an env variable so the CLI can find it from any directory:

export CRM_DATA=/path/to/your/crm/folder

And I added some handy shell aliases. Write the core logic in Python, but use Unix to glue it all together. The core trick is piping crm list

into fzf

to pick a record, then acting on it:

alias crm='uv run --project /path/to/crm crm'

crme() {
  local code
  code=$(crm list "$@" | grep 'β”‚' | fzf --ansi | awk -F 'β”‚' '{gsub(/ /, "", $2); print $2}')
  [[ -n "$code" ]] && vim "$CRM_DATA/contacts/${code}.md"
}

Same pattern, different verb. Once it clicked I added a small family of them:

Alias Action
crmg fzf pick β†’ crm get (details)
crme fzf pick β†’ open in vim (active only)
crmc fzf pick β†’ intake call (template + editor + reminder)
crms fzf pick β†’ coaching check-in (template + editor + reminder)
crma fzf pick β†’ add reminder
crmr open reminders.md in vim

I even added this to my .vimrc

to quickly add a note with the current date:

nnoremap <Leader>da o- <C-R>=strftime('%Y-%m-%d')<CR> β€”

Why This Works #

No schema migrations. Adding a field means updating a template and the code that reads it. The existing files still work.

No database / ORM. Load the file, parse it, modify, write back. Atomic at the file level.

No deployment. It's a CLI. I run it locally.

No vendor lock-in. It's Markdown in a folder. No platform, no service, no terms of service deciding what happens to my data.

Debuggable by inspection. When something looks wrong, I open the file and read it.

The entire codebase fits in my head. That's the goal. When software is small enough to understand completely, changes become cheaper and maintenance becomes predictable.

I've built the heavier version before: a multi-user Django CRM with authentication, workflows, and reporting. Those requirements justified the complexity. This project didn't have those requirements.

Building the heavy version first taught me what to cut here. Yes, this version is single-user and local-only, and that's a real limitation. But it's exactly as much software as I need, which matters more than ever now that LLMs will happily generate ten times that.

To be clear, I'm not saying Markdown files and shell aliases are a common candidate for CRMs. It was the right fit for my use case. The point isn't Markdown and shell aliases, it's the thinking: start with design, then guide the LLM. Architecture follows requirements, and for every new dependency or tool addition, ask whether the requirements justify it. In enterprise software the stakes are higher and the systems are bigger, but the questioning is identical, and AI can't supply the experience, taste, and judgment that answer it.

The Approach #

If you're stuck on a blank page, try this:

Write the spec first. Not code, not a database schema. What problem are you solving? What's explicitly out of scope? - List your constraints. Single user? Local only? CLI acceptable? Each "yes" removes a layer of complexity. - Pick the most boring storage. Often you think you need a database, but for small data, consider plain text files. They're human readable, editable, searchable, and you can build just the logic you need to manage them. - Count your dependencies. Every library is code you'll maintain forever. Two is better than twenty. - Ship something you can use. Define the first MVP;mind mapping helps with this: my first version had a products index, contacts and notes. Reminders, templates and shell aliases came later.

The simplest thing that works isn't a stepping stone to the "real" version. Often, it is the real version.

Most tools I've built that survived started small and stayed understandable. The ones that failed usually began with ambitious architectures and hypothetical requirements.

Build the simplest thing that solves today's problem. If the problem grows, the software can grow with it. If it doesn't, you'll already have the right amount of software.

Shipping fast with AI is the easy part. Knowing what to keep, rewrite, and trust is the hard part. I work with developers 1:1 to audit AI-built codebases, trace the real control flow, and make them something you can explain and own, without leaning on a chatbot. How 1:1 coaching works β†’

── more in #developer-tools 4 stories Β· sorted by recency
── more on @bob belderbos 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/build-the-simplest-t…] indexed:0 read:6min 2026-06-02 Β· β€”