LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Side Projects That Ship: Scoping, Finishing, and Launching Small Tools

careerside-projectsproductivityshippingindie-hackingsoftware-engineering

Every developer has a graveyard. A ~/projects folder full of directories that represent the first three excited weekends of some idea, then silence. A half-built CLI tool. A database schema for the app that was going to replace Notion. A scraper that worked once. A game that got as far as a moving sprite.

This is universal and it’s not a character flaw. Side projects have a natural attractor state: stuck at 80%, not bad enough to delete, not finished enough to use. The graveyard grows.

What separates the projects that actually ship from the ones that don’t is rarely talent, time, or ideas. It’s a set of specific, learnable decisions made before and during the build. This guide covers those decisions.


The Single Most Important Reframe

Most developers think about side projects as things they build and then maybe ship. The mental model is: build → (optionally) ship.

The model that produces shipped projects is the reverse: ship → then keep building.

This sounds like a platitude until you actually sit with what it implies. If shipping is the goal from day one, every scope decision is filtered through “does this get me to something shippable, or does it delay it?” The answer restructures your entire build.

A notes app that ships on day 30 as a single-file web app with localStorage gets used, gets feedback, and either grows or gets abandoned with something learned. A notes app planned to eventually have real-time sync, plugins, a mobile app, and an AI assistant ships never.

The shipped version wins even if it’s embarrassing. The unshipped version wins nothing.


Scoping: The Skill Nobody Teaches

Scoping is the practice of deciding what a project is not. Most side projects fail in scoping — not because the developer can’t build, but because the scope keeps expanding to match the full vision before anything ships.

The Feature Triage

When you have an idea, write down everything it could do. All of it. Then go through the list with a brutal filter: which of these is required for the core value to exist?

Not “would be nice.” Not “users will want.” Required for the core value to exist.

For a CLI tool that generates boilerplate for new projects:

  • Generates a basic file structure — required
  • Reads a config file for preferences — required
  • Supports 3 languages — maybe, but maybe 1 ships faster
  • Has a TUI with colors and spinners — nice, not required
  • Works as a VS Code extension too — not required
  • Has a web UI — not required
  • Syncs templates to the cloud — not required
  • Supports team sharing — not required

Version 1.0 is the first bullet, possibly the second. Everything else is v1.1, v2.0, or never.

The question to ask every time a feature creeps in: “If this wasn’t there, would a real person still get value from using this?” If yes, it’s optional. Cut it.

The Smallest Useful Version

Define the Smallest Useful Version (SUV) before you write a line of code. This is not the smallest possible version — it’s the smallest version that genuinely solves the problem for at least one real person (including yourself).

Write it down explicitly:

SUV: A command-line tool that takes a URL and returns a summarized list of tasks extracted from the page. No login, no storage, no configuration. Input URL, get task list, exit.

Everything outside this definition is out of scope for v1. Not forever — just for v1. You will be tempted. The definition is the anchor.

Time-Boxing the Build

Pick a deadline before you start, not when you get stuck. The deadline should be slightly uncomfortable — long enough to build the SUV, short enough that scope creep has nowhere to hide.

For most weekend projects: 2–4 days of actual work. For most solo evening projects: 2–3 weeks of evenings. For more complex tools: 4–6 weeks maximum before a v0 ships to at least one person.

The deadline is not when the project is “done.” It’s when something ships. Done is a trap — software is never done.


Technology Choices for Shipping

The technology choice is where a lot of projects die. Developers use side projects as a chance to learn something new, which is great, but “learn new tech” and “ship something” are competing goals. When they’re both the goal, neither happens.

The Boring Technology Principle

Use the most boring technology that solves the problem. The goal of a side project that ships is to ship, not to evaluate Rust vs. Zig for CLI performance. That’s a different kind of project.

If you know Python, the CLI is Python. If you know Node, the web app is Express + vanilla JS. If you know Go, the server is Go. “Boring” here means “the thing you can build fastest with the tools you know best.”

The corollary: if learning a new technology is the actual goal of the project, that’s fine — but acknowledge it. Expect the project to take 3× longer and have half the chance of shipping. Not bad, just honest.

The Single-File Start

Starting with the simplest possible architecture — one file, no framework, no database — removes friction at the most vulnerable phase of a project: the beginning. A project that starts as a single Python script has a better chance of shipping than one that starts with a monorepo, a CI pipeline, and three services.

You can always refactor later. You can’t ship what you never built.

Single-file CLI (Python):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
"""
my-tool — does the thing
Usage: my-tool <arg>
"""
import sys
import argparse

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", help="Input to process")
    args = parser.parse_args()

    result = do_the_thing(args.input)
    print(result)

def do_the_thing(input: str) -> str:
    # The actual logic lives here
    return f"processed: {input}"

if __name__ == "__main__":
    main()

Single-file web app (Python + Flask):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask, request, jsonify, render_template_string

app = Flask(__name__)

HTML = """<!DOCTYPE html>
<html>
<head><title>My Tool</title></head>
<body>
  <form method="POST">
    <input name="input" placeholder="Enter something">
    <button type="submit">Go</button>
  </form>
  {% if result %}<p>{{ result }}</p>{% endif %}
</body>
</html>"""

@app.route("/", methods=["GET", "POST"])
def index():
    result = None
    if request.method == "POST":
        result = do_the_thing(request.form["input"])
    return render_template_string(HTML, result=result)

def do_the_thing(input: str) -> str:
    return f"processed: {input}"

if __name__ == "__main__":
    app.run(debug=True)

This runs. You can show it to someone today. That’s the point.

When to Add Infrastructure

Add complexity when you can’t avoid it — not before:

Problem you hit What to add
Need to persist data SQLite (not Postgres, not Redis)
Need user accounts A single JWT secret + bcrypt
Need background jobs A queue in SQLite + a worker loop
Need real-time updates Server-sent events (SSE)
Need to deploy A single VPS, a Dockerfile
Need a database in the cloud Turso, PlanetScale, or Supabase free tier

The pattern: use the simplest thing that solves the immediate problem. Upgrade only when you’re constrained.


The 80% Problem: Getting Unstuck

Almost every side project stalls at roughly 80% done. The exciting core is built, it works, and now there’s a list of things that need to happen before you can “really” ship: edge cases, polish, documentation, deployment setup, that one feature that feels essential.

This is the moment most projects die. Understanding why it happens is the first step to getting past it.

Why 80% Is a Trap

The 80% phase lacks the dopamine of the early build. The first 60% of a project is all new: new features, new problems, new things working for the first time. The last 20% before ship is slog: error handling, testing edge cases, writing a README, setting up a domain, packaging.

The mind drifts to new project ideas, which are all promise and no slog. The unfinished project represents past investment (sunk cost) plus future work (obligation). The new idea represents only future excitement.

The counterforce: don’t start the next project until the current one ships. This is a hard rule and worth keeping hard.

The Completion Checklist

When you feel stuck at 80%, make the path to done concrete. Write down every remaining task that stands between you and shipping. Not someday tasks — actual blockers. Then ask, for each one:

  1. Is this actually required for v1?
  2. If I cut this, would a real user still get value?

For anything that answers “no” to #1 or “yes” to #2: move it to a v1.1 list and remove it from the blocker list. You’ll often find that 80% is actually 95% when you’re honest about what’s actually required.

Then estimate the remaining tasks. Not in hours — in 2-hour blocks. If the remaining work is 4 two-hour blocks, you ship in two weekends. Having a concrete number changes “this might go on forever” to “I have 8 hours of work left.”

The Fake Ship

If you’re really stuck, deploy a fake version. Put up a landing page that describes what the tool will do. Share it with two people you know. Get their reaction.

This does something psychologically powerful: it makes the project real. It has a URL. Someone has seen it. The project exists in the world in a way that a local folder doesn’t. Suddenly finishing it feels different — there are people who know about it.


Shipping: What It Actually Means

“Ship” means different things at different stages. You don’t need a polished landing page, HackerNews post, and Product Hunt launch to have shipped something.

Tier 1: Shipped to Yourself

The project is deployed somewhere, running, and you use it. This is harder than it sounds and more valuable than it seems. A tool you actually use generates real feedback — you discover what’s missing, what’s annoying, what you reach for that isn’t there. Running in production (even if production is just your own laptop) changes how you think about the project.

Tier 2: Shipped to One Person

Share it with one person who isn’t you and who has the problem it solves. Watch them use it if you can. Listen to what confuses them. This is more valuable than any amount of internal testing.

Getting to Tier 2 doesn’t require a polished README, a domain name, or a launch. It requires a working thing and an email or Slack message.

Tier 3: Publicly Available

The project is on the internet, findable by strangers. This could be:

  • A GitHub repo with a working README
  • A CLI on PyPI or npm
  • A web app at a real URL
  • A VS Code extension in the marketplace

Tier 3 is where most developers aim when they say “ship,” but it’s actually Tier 3 on a ladder that starts at Tier 1. Get to Tier 1 first. Then Tier 2. Tier 3 follows naturally.

The README as Ship Requirement

Before Tier 3, write a README that includes:

  1. What the project does (one sentence)
  2. Who it’s for
  3. How to install it
  4. One working example

That’s it. No philosophy, no roadmap, no “contributions welcome” boilerplate. If a stranger can read those four things and run the example, you’re ready.

1
2
3
4
5
6
7
# quicksum

Summarizes the action items from any webpage in your terminal.

For developers who don't want to read a 2,000-word article to find the three things it asks them to do.

## Install

pip install quicksum


## Usage

quicksum https://example.com/some-long-article


Output:

• Update your nginx config to use HTTP/2 • Restart the service after making changes • Test with curl -I https://your-domain.com

That’s a shippable README.


After Shipping: What Next

Once something is publicly available, you face a fork: build more, or move on.

When to Keep Building

Keep building if:

  • People are using it and asking for specific things
  • You’re using it yourself and keep hitting limitations
  • The core problem is bigger than your v1 addressed and you’re still interested in it

When you keep building, the same rules apply: scope each iteration, define what ships, time-box it.

When to Declare Victory and Move On

Move on if:

  • Nobody is using it despite reasonable exposure
  • You’ve lost genuine interest in the problem
  • The v1 solved the problem well enough that there’s nothing worth adding

Declaring victory is underrated. A shipped, finished project — even a small one — is a real thing in the world. It taught you something. It solved a problem. It’s done.

The graveyard is for unshipped projects. Finished projects belong in a portfolio, a resume, a README you’re proud of, or a conversation you have with someone about what you’ve made.


A Pattern That Works

Here’s a concrete workflow that reliably produces shipped projects:

Day 0: Define Write three things down:

  1. The one-sentence description of what it does
  2. The Smallest Useful Version (SUV) — exactly what v1 does and does not do
  3. The ship deadline — a specific date

Days 1–N: Build the SUV Work only on what’s in the SUV definition. When a new idea occurs to you, write it in a “future” list and keep building. Stop adding until v1 ships.

N-2 days before deadline: Stop adding, start finishing Close the feature list. Everything from here is bug fixes, edge cases, and the README.

Deadline: Ship Deploy it, share it with at least one person, and write down what you learned.

One week later: Decide Is anyone using it? Are you using it? Does it have a future? Make the call: keep going, or declare it done and start the next thing.


Small Wins Compound

A developer who ships six small tools per year — a CLI, a script, a browser extension, a tiny web app — builds something the developer who never ships doesn’t: a pattern of completion. The shipped projects get easier because the muscle is developed. The scope decisions get faster. The “just get it out” tolerance for imperfection grows.

More concretely: six shipped projects per year is a portfolio. It’s demonstrated ability to take an idea from concept to something real. That’s rare, and it’s worth more in almost every professional context — interviews, freelance work, reputation — than any amount of in-progress work.

The goal isn’t to ship something perfect. It’s to ship something. Then ship another. That’s the whole game.

Comments