I want to tell you about three projects I spent hundreds of hours on that nobody ever used. Not because I ran out of time. Because I never shipped them.
The three projects
Project 1: Freelancer time tracker
Core timer working within two weeks. Then: reporting dashboard, CSV export, team features, client portal — for a product with zero users. Archived when I changed jobs.
Project 2: Notion template marketplace
60% done before the backend rewrites started. "The architecture wasn't clean enough." Month four: clean code, no product, no energy.
Project 3: AI habit tracker
Built it, used it myself for two weeks, stopped when I couldn't answer "why would anyone pay for this?"
Three codebases on GitHub. Zero shipped products.
The pattern I kept missing
I was optimizing for building, not for shipping.
Without a hard external forcing function, the project always slipped. Re-entry friction accumulated. Every session started with: what do I work on today? If that question took 20 minutes to answer — or worse, if I answered it wrong and spent 3 hours on something that didn't move the needle — I'd slowly start avoiding the project altogether.
Eventually it entered "almost done permanently." Touching it meant confronting how much was left. So I just didn't.
How I fixed it — the technical side
I built MVP Builder: a structured 13, 21 or 30-day sprint with one focused daily prompt for the next day, delivered the evening before.
The constraint: Vercel free tier allows max 2 cron jobs, minimum daily interval.
My solution — two cron jobs covering both hemispheres:
0 6 * * * → EU: CET 07:00, CEST 08:00
0 14 * * * → Americas: MST 07:00, CST 08:00, EST 09:00
Timezone filtering (app-side, not cron-side)
import { toZonedTime } from 'date-fns-tz';
function isInDeliveryWindow(timezone) {
try {
const zoned = toZonedTime(new Date(), timezone);
const hour = zoned.getHours();
return hour >= 7 && hour < 9;
} catch {
return true; // fail-open: send rather than skip
}
}
Why app-side filtering instead of more cron jobs? Because cron runs at a fixed UTC time — it can't know each user's local hour. The filter runs per-user at send time.
Why fail-open? An invalid timezone (typo, legacy string) shouldn't mean the user never gets their prompt. Missing one day is worse than getting it slightly off-window.
Idempotency: 20h lookback, not 24h
const twentyHoursAgo = new Date(Date.now() - 20 * 60 * 60 * 1000);
const { data: alreadySent } = await supabase
.from('users')
.select('id')
.eq('id', userId)
.gt('last_prompt_sent_at', twentyHoursAgo.toISOString())
.single();
if (alreadySent) return; // already sent today
Why 20h and not 24h? DST transitions. A 24h lookback on a DST-switch day can either skip a user or double-send. 20h is safely within any DST shift (max ±1h) while still preventing duplicates within the same day.
Timezone capture at signup (silent, no UI change)
const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
// → include in form payload, no UI element needed
One line. No dropdown. No user decision fatigue. Works on every modern browser.
The schema (relevant columns)
ALTER TABLE users ADD COLUMN timezone TEXT DEFAULT 'Europe/Berlin';
ALTER TABLE users ADD COLUMN last_prompt_sent_at TIMESTAMPTZ;
The DEFAULT 'Europe/Berlin' matters: new users without a detected timezone still get a prompt at a reasonable hour rather than silently skipping.
What's different this time
I shipped it.
$0 MRR. The beta was free. The product that helps developers ship has to ship first — otherwise the whole premise falls apart.
Three modules, based on where you actually are:
- Bronze (13 days): just an idea → something someone can use
- Silver (21 days): started but stuck → live
- Gold (30 days): almost done → over the line
Each evening: one focused prompt for the next day, 30–45 minutes of work. No sprawling to-do lists. At every milestone a person reads the proof you submit. That's it.
Five click questions tell you which module fits: mvpbuilder.io/go
If you've shipped something after a long struggle — or if you're currently stuck at "almost done" — drop it in the comments. I read every one.
Building in public. Will post updates on what the beta looks like.
Top comments (0)