From Zero to Shipped: The Solo Developer's Guide to Scope Discipline
Let me tell you a story. In 2024, I decided to build a SaaS product completely solo. I had a vision for a revolutionary analytics platform that would seamlessly blend event tracking, session replay, and AI-driven insights. It was going to be massive.
Six months later, I had a beautiful landing page, a highly scalable Kubernetes cluster, a custom-built event ingestion pipeline using Rust and Kafka... and zero customers. I had fallen into the most dangerous trap a solo developer can face: a total lack of scope discipline.
I was building for a hypothetical scale I didn't have, adding features nobody asked for, and treating every engineering decision as if I were leading a team of 50 at a FAANG company. I eventually scrapped the whole project, took a month off, and started over with a new mindset.
In this post, I want to share the technical and psychological frameworks I use now to maintain relentless scope discipline. This is the guide I wish I had when I started.
The Architecture of Pragmatism
When you are a solo developer, your most precious resource isn't CPU cycles or database throughput; it's your own time and mental bandwidth. Every line of code is a liability. Every new moving part in your infrastructure is something that can wake you up at 3 AM.
Choosing the Boring Stack
The first rule of scope discipline is to choose boring technology. When I restarted my project, I threw out Rust, Kafka, and Kubernetes.
I went with a monolith. Next.js on the frontend and backend, deployed to Vercel. A single PostgreSQL database hosted on Supabase. That's it.
Here is why a monolith is a superpower for solo devs:
- Zero network boundaries: You don't have to serialize/deserialize data between microservices.
- Shared types: You can share TypeScript interfaces between your frontend components and your database ORM (I highly recommend Prisma for this).
- Atomic deployments: When you deploy, the whole system goes out together. No version mismatch headaches.
// Shared type definition in a single repo import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // The backend API route (Next.js App Router) export async function POST(req: Request) { const data: CreateUserRequest = await req.json(); const user = await prisma.user.create({ data: { email: data.email, name: data.name } }); return Response.json(user); } // The frontend component seamlessly using the same type async function createUser(payload: CreateUserRequest) { const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(payload) }); return res.json(); }
This setup reduces cognitive load. You aren't context-switching between different languages or deployment pipelines. You are just building the product.
The "Hardcoded" Feature Strategy
Scope creep often happens because we try to build generic solutions to specific problems.
For example, I needed a way to send onboarding emails. The "engineering" way to do this is to build an entire email templating system, a cron-based scheduler, and a UI to manage campaigns. That’s a 3-week project.
The "solo dev scope discipline" way is to hardcode it until it breaks.
// A perfectly acceptable V1 onboarding email system export async function sendWelcomeEmail(userEmail: string) { const html = ` <h1>Welcome to our app!</h1> <p>Here are three things you should try first...</p> `; await resend.emails.send({ from: 'me@myapp.com', to: userEmail, subject: 'Welcome onboard', html: html }); }
Is this scalable? No. If marketing wants to change the email copy, they have to ask me to push a code change. But there is no marketing team. It's just me. By hardcoding this, I saved 3 weeks of dev time and shipped the feature in 10 minutes.
You should delay building admin panels, CMS integrations, and configuration UIs for as long as humanly possible. If a database script can do the job, use the script.
The 80/20 Rule of UI Components
As engineers, we love building pixel-perfect, highly reusable UI components. We will spend days building the ultimate highly-configurable <Dropdown /> component that handles asynchronous loading, multiselect, keyboard navigation, and virtualized lists.
Stop.
If you need a dropdown, use a library like Radix UI, Headless UI, or shadcn/ui. Better yet, just use a native <select> tag if it's an internal tool or a low-priority page.
// Don't build this: // <SuperComplexDropdown items={items} onSelect={...} allowMultiselect virtualize /> // Just use standard HTML until users complain: <select className="border p-2 rounded" onChange={(e) => handleSelect(e.target.value)} > {items.map(item => ( <option key={item.id} value={item.id}>{item.name}</option> ))} </select>
Your users don't care if your dropdown is custom-built. They care if your product solves their problem. Borrowing UI components allows you to focus your limited time on your core value proposition.
Feature Toggles: Your Safety Net
When you do have to build a complex feature, use feature toggles (feature flags). This is crucial for solo developers because it decouples deployment from release.
You can merge messy, half-finished code into production as long as it's hidden behind a flag. This prevents long-lived feature branches, which are notorious for causing horrific merge conflicts when you finally try to integrate them.
// Simple environment-based feature flag const isFeatureXEnabled = process.env.NEXT_PUBLIC_ENABLE_FEATURE_X === 'true'; export default function Dashboard() { return ( <div> <h1>Dashboard</h1> {isFeatureXEnabled && <ExperimentalAnalyticsPanel />} </div> ); }
This allows you to test in production, show the feature to a few select beta testers, and back out instantly if something breaks, without needing to roll back the entire deployment.
Conclusion: The Art of Saying No
Scope discipline is fundamentally the art of saying "no." Saying no to cool new frameworks. Saying no to edge cases that affect 1% of users. Saying no to architectural purity in favor of shipped features.
When you are flying solo, momentum is everything. If a feature takes longer than two weeks to build, your scope is too large. Cut it in half. Ship the ugly, hardcoded, unscalable version first. See if anyone actually clicks the button. If they do, then you have permission to over-engineer it later.
From zero to shipped is a marathon, but you run it by taking very small, very ugly steps. Embrace the pragmatism, choose the boring stack, and get your product into the hands of users.
Newsletter
Occasional essays on engineering, design, and building resilient systems. No spam.