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 30-day sprint with one focused daily prompt, delivered 07:00–09:00 local time.
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. Beta cohort is free. The product that helps developers ship has to ship first — otherwise the whole premise falls apart.
Three tiers based on where you actually are:
- Bronze (13 days): Idea → working prototype
- Silver (21 days): Started but stuck → shippable product
- Gold (30 days): Almost done → actually shipped
Each morning: one focused prompt, 30–90 minutes max. No sprawling to-do lists. One milestone checkpoint with proof of work. That's it.
Cohort #1 is free. 5–8 spots: mvpbuilder.io/pipeline
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 cohort looks like.
Top comments (0)