{"slug": "build-the-simplest-thing-that-works", "title": "Build the Simplest Thing That Works", "summary": "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.", "body_md": "# Build the Simplest Thing That Works\n\n*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 →*\n\nI 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.\n\nBefore writing any code, I focused on the constraints first.\n\nThe final solution ended up being two Python files, two dependencies, and a folder of Markdown.\n\n## The Temptation to Over-Engineer\n\nWhen we developers hear \"CRM,\" we reach for the full stack: database schema, user auth, web dashboard, API layer, deployment pipeline.\n\nBefore writing a line of code, we've committed to thousands of lines that could become tomorrow's technical debt.\n\nDave Thomas puts it bluntly in his book [ Simplicity](https://pragprog.com/titles/dtcode/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.\n\nAI 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.\n\nI'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.\n\n## The Constraints That Set Me Free\n\nSo what's the simplest thing that actually works? In [Start with Design](https://belderbos.dev/blog/design-over-code/), 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.\n\nIn my CRM case, doing the scoping exercise first revealed a few important constraints:\n\n**Single user.** It's my CRM. No auth, no permissions, no multi-tenancy.\n\n**CLI-first.** I live in the terminal. A web UI would be overhead I don't need.\n\n**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.\n\n**Minimal surface.** The data never leaves my machine. No network calls, no third party storing my contacts.\n\nNotice what happened here. Every constraint removed an entire class of problems:\n\n- Single user → no auth\n- Local only → no hosting\n- Files → no database\n- CLI → no frontend\n\nGood architecture is often less about what you build and more about what you decide not to build.\n\n## What I Actually Built: A CLI CRM in Two Files\n\nTwo Python files. Two dependencies: Typer for the CLI, Rich for table formatting. The repo is [here](https://github.com/bbelderbos/simple_crm).\n\nThat 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.\n\nData lives in a local folder as plain Markdown:\n\n```\ncrm/\n  products.md        # Simple table: code, name, price\n  reminders.md       # Due date, contact, description\n  contacts/\n    jd1.md           # Jane Doe (unique ID)\n    bs2.md           # Bob Smith\n```\n\nEach contact file is structured markdown I can open in any editor:\n\n```\n# Jane Doe\n- some metadata fields -\n\n## Notes\n- 2026-05-28 — ...\n- 2026-05-31 — ...\n```\n\n## Python for Logic, Unix for Glue\n\nThe data folder I expose via an env variable so the CLI can find it from any directory:\n\n```\nexport CRM_DATA=/path/to/your/crm/folder\n```\n\nAnd 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`\n\ninto `fzf`\n\nto pick a record, then acting on it:\n\n```\n# uv can run from anywhere!\nalias crm='uv run --project /path/to/crm crm'\n\n# Interactive contact picker -> open in vim (active only)\ncrme() {\n  local code\n  code=$(crm list \"$@\" | grep '│' | fzf --ansi | awk -F '│' '{gsub(/ /, \"\", $2); print $2}')\n  [[ -n \"$code\" ]] && vim \"$CRM_DATA/contacts/${code}.md\"\n}\n```\n\nSame pattern, different verb. Once it clicked I added a small family of them:\n\n| Alias | Action |\n|---|---|\n`crmg` | fzf pick → `crm get` (details) |\n`crme` | fzf pick → open in vim (active only) |\n`crmc` | fzf pick → intake call (template + editor + reminder) |\n`crms` | fzf pick → coaching check-in (template + editor + reminder) |\n`crma` | fzf pick → add reminder |\n`crmr` | open reminders.md in vim |\n\nI even added this to my `.vimrc`\n\nto quickly add a note with the current date:\n\n```\nnnoremap <Leader>da o- <C-R>=strftime('%Y-%m-%d')<CR> —\n```\n\n## Why This Works\n\n**No schema migrations.** Adding a field means updating a template and the code that reads it. The existing files still work.\n\n**No database / ORM.** Load the file, parse it, modify, write back. Atomic at the file level.\n\n**No deployment.** It's a CLI. I run it locally.\n\n**No vendor lock-in.** It's Markdown in a folder. No platform, no service, no terms of service deciding what happens to my data.\n\n**Debuggable by inspection.** When something looks wrong, I open the file and read it.\n\nThe 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.\n\nI'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.\n\nBuilding 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.\n\nTo 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.\n\n## The Approach\n\nIf you're stuck on a blank page, try this:\n\n-\n**Write the spec first.** Not code, not a database schema. What problem are you solving? What's explicitly out of scope? -\n**List your constraints.** Single user? Local only? CLI acceptable? Each \"yes\" removes a layer of complexity. -\n**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. -\n**Count your dependencies.** Every library is code you'll maintain forever. Two is better than twenty. -\n**Ship something you can use.** Define the first MVP;[mind mapping helps with this](https://belderbos.dev/blog/mind-map-software-project-scope/): my first version had a products index, contacts and notes. Reminders, templates and shell aliases came later.\n\nThe simplest thing that works isn't a stepping stone to the \"real\" version. Often, it *is* the real version.\n\nMost tools I've built that survived started small and stayed understandable. The ones that failed usually began with ambitious architectures and hypothetical requirements.\n\nBuild 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.\n\nShipping 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 →](/coaching/#own-project)", "url": "https://wpnews.pro/news/build-the-simplest-thing-that-works", "canonical_source": "https://belderbos.dev/blog/build-the-simplest-thing-that-works/", "published_at": "2026-06-02 00:00:00+00:00", "updated_at": "2026-06-17 10:01:35.544246+00:00", "lang": "en", "topics": ["developer-tools", "ai-products"], "entities": ["Bob Belderbos", "Typer", "Rich", "GitHub", "Dave Thomas"], "alternates": {"html": "https://wpnews.pro/news/build-the-simplest-thing-that-works", "markdown": "https://wpnews.pro/news/build-the-simplest-thing-that-works.md", "text": "https://wpnews.pro/news/build-the-simplest-thing-that-works.txt", "jsonld": "https://wpnews.pro/news/build-the-simplest-thing-that-works.jsonld"}}