What ships, when.
This page is rendered from the same lib/roadmap.ts that drives Mission Control. When items move there, they move here. No spin, no marketing edits.
foundation
social + economy
production hardening
real product loop
open invite
scoring maturity
quality polish
wider access
full release
ongoing iteration
alpha 1
you + Claude shipping the first usable surface
- DONE Google OAuth + magic-link auth — Direct Google OAuth shipped — consent screen branded millefold.com (not the supabase.co project URL). Supabase fallback removed once verified. Magic-link still live as email-only fallback.
- DONE Dashboard hero + KPI tiles + sparklines
- DONE Assets + liabilities CRUD + CSV import
- DONE Net-worth snapshots + history chart
- DONE Multi-currency (Frankfurter ECB rates)
- DONE Settings: country + default currency
- DONE Mission Control + admin gate
- DONE Newsletter sender + waitlist
- DONE OG / L11 / L12-architect badges
- DONE First security audit pass
alpha 2
first invited users
- DONE Public profiles + slug + avatar upload
- DONE Friends / opt-in shared tracking pages
- DONE Wishlist (aspirational items)
- DONE Saved pies (named portfolio views)
- DONE Founder cohort + slot economy (50 base + 10/referral)
- DONE Referral attribution via ?ref=<slug>
- DONE Per-profile OG images for shares
alpha 3
infrastructure layer
- DONE /privacy + /terms + /docs + /contact + /status pages
- DONE GDPR data export + account deletion
- DONE Per-user newsletter unsubscribe tokens
- DONE Error pages (404 / 500 / global-error)
- DONE Onboarding ladder on first dashboard load
- DONE Email provider swap to Brevo + live usage panel
- DONE Brevo: signup + domain + API key in Wrangler
- DONE Mission Control password gate (24h HMAC session)
- DONE Supabase service role key in Wrangler
- DONE VAPID keys for Web Push
- DONE Weekly digest cron via GitHub Actions
- DONE Cloudflare WAF rate-limit + scanner-probe rules
- DONE Admin role granted to business identity (hello@millefold.com)
- DONE Full security audit pass (Supabase linter clean)
- DONE Zero-knowledge encryption at rest (AES-256-GCM, per-user keys) — Every sensitive ledger value (asset/liability names + amounts, snapshot totals, wishlist, pies) is stored only as ciphertext — plaintext columns dropped 2026-06-01 after a verified read-switch. The DB / a service-role compromise / a subpoena yields ciphertext, not net worths. App decrypts per-user in the Worker; reads, writes, crons + GDPR export all encrypted-only.
alpha 4
5-20 invited users actually using it daily
- DONE Real scoring algorithm — replace stub — Flexible discipline score live (lib/scoring.ts computeDisciplineScore): weighted average over only the inputs applicable to each user — inapplicable ones (no debt, income/dividend tracking not wired, <3-day history) drop out and their weight is redistributed, so nobody is penalised for a feature they don't use. net_worth_ratio / debt_quality / allocation_spread / habit_consistency compute today; savings_rate + reinvestment light up when income/dividend tracking lands. Weights 25/20/15/20/10/10. Displayed breakdown + the number are one source of truth.
- DONE Snapshot history filling out + insights surfacing — Range toggle (7d/30d/90d/all), milestones crossed list, biggest swing callout — all live on dashboard.
- DONE Error monitoring (Sentry vs PostHog vs Highlight) — PostHog EU live — server-side captures via fetch in instrumentation.ts ($exception_list format), client-side via posthog-js with capture_exceptions. Vendor picked, smoke-tested, key in Wrangler.
- DONE Donate button (optional donations) — Buy Me a Coffee live at /support — slug buymeacoffee.com/millefold. Donate link in landing footer + dedicated /support page in megaeth-raw aesthetic.
- IN_FLIGHT Dashboard design polish (Snowball + StockEvents inspired) — Shipped sparklines + KPI tiles + Upcoming + holdings table + donut + bottom mobile nav. Iteration based on actual usage.
- DONE Portfolio history backfill — manual + CSV — /dashboard/history: pick past date + assets/liabs OR upload date,net_worth CSV. Bulk upsert. Existing-date rows overwrite.
- DONE Long-range projection chart (20-40y) — ProjectionPreview now a curve chart with horizon slider (5-40y), monthly contribution + return inputs, milestone cards at +10/+20/+30/+40y.
- IN_FLIGHT Per-category history + projection filter — History filter SHIPPED — encrypted per-category breakdown captured per snapshot (Option A: breakdown_enc JSON blob) + capture-on-mutation; history chart filters by asset category. Remaining: projecting a single category forward (needs per-category growth assumptions — folds into the personal-projection work).
- DONE Personal projection — weighted historical return from your holdings (founder cohort) — Shipped + verified live 2026-06-09. lib/personal-projection.ts computes the blend — broker-ticker holdings get a real 10y price CAGR (Yahoo Finance dailies cached in market_price_history, price-only ex-dividends; Stooq demoted to fallback after its anti-bot wall was found to have silently broken ALL price fetching), the rest use stated per-class assumptions; weights are FX-converted values, computed server-side (values encrypted at rest). PERSONAL preset + holding-by-holding breakdown table on /dashboard/projection. Available to founder-cohort accounts (lib/entitlements.ts); other accounts see a neutral locked state.
- DONE Custom portfolio sandbox — what-if path comparison (founder cohort) — Shipped + verified 2026-06-09: /dashboard/sandbox races CURRENT vs ALT A vs ALT B on one chart + end-state table (final value, Δ vs current). Slices from your holdings (keep their HIST/ASSUMED rates), asset classes (stated assumptions), live ticker lookup (real 10y price CAGR via server action — Yahoo-backed, verified end-to-end from prod egress), or custom rates; weight inputs + normalize-to-100. Same entitlement gate as PERSONAL. Engine + edge cases verified via scripts/verify-sandbox.mts. Not yet: drag-and-drop, saving portfolios.
- DONE Account email verification + recovery flow polish — Callback returns specific error codes, login page surfaces them, check-email state shows the address + resend cooldown + lost-access escape hatch.
- DONE Activity feed for friends (level-ups visible) — activities table + SECURITY DEFINER feed function. /dashboard/activity shows my events + followees' level-ups / OG claims / follows.
beta 1
first 100-1000 founders (cohort filling)
- TODO Frictionless onboarding flow (3-step max)
- IN_FLIGHT Founder cohort filling milestone tracking + visible counter
- DONE Pagination + optimistic UI on lists
- TODO First-1000 L8+ OG badges starting to fan out
- IN_FLIGHT Public roadmap iterated from real user feedback
- TODO Bug bash from observed errors (after error monitoring lands) — Unblocked — error monitoring (PostHog EU) is live. Triage real captured exceptions once invited users generate traffic.
beta 2
active 1000-cohort users
- BLOCKED Real scoring algorithm tuned with first-1000 data
- TODO Score insights — what's driving your number + how to improve
- DONE Web Push activated (notifications on level-up + OG claim) — Verified end-to-end in Firefox 2026-05-20 — VAPID JWT + RFC 8291 aes128gcm via Web Crypto, real notification landed via Mission Control test. Fires on level-up + OG claim via Next after(). Chrome has its own FCM connectivity quirks on some devices; Firefox path is the reliable reference.
- IN_FLIGHT Weekly digest emails firing to opted-in subscribers — Cron live, route works, no scheduled sends yet because no eligible subscriber history
- TODO More category nuance + sub-categories
beta 3
beta users + a11y testers
- IN_FLIGHT i18n full translation rollout (LT first, then NB/PL/SV/DE) — Scaffold + EN/LT dictionaries shipped. Translation of every public page coming.
- TODO Customizable / draggable dashboard layout — Users move widgets around interactively (or via settings) — what to show on top, what to hide. Trading-212-style flexibility.
- IN_FLIGHT Mobile responsive 3rd pass + tap-target audit
- TODO Accessibility audit (WCAG AA)
- TODO Performance audit (Lighthouse, edge caching, image sizes)
- IN_FLIGHT Help docs expanded with workflows + screenshots
- TODO Public feedback form on /roadmap
pre-release
all beta users
- TODO PERSONAL projection + sandbox beyond the founder cohort — Planned. Both are currently limited to founder-cohort accounts (the first 1,000 sign-ups); how they open up to other accounts is not decided yet.
- TODO Slot caps revisited for non-cohort accounts
- TODO PDF upload + AI extraction — Planned: upload broker or bank PDF statements from sources that don't export CSV and turn them into ledger rows. Upload pipeline first, extraction later.
1.0
anyone
- BLOCKED Connected broker accounts (SnapTrade or Plaid)
- BLOCKED Verified L11 arbitrage mechanic activates (real broker data)
- TODO Live market prices replacing manual current_value
- TODO Dividend events calendar (Snowball-style)
- TODO Public launch — content + SEO + marketing site polish
- IN_FLIGHT Customer support flow scaled (status page + support inbox)
- IN_FLIGHT Backup + disaster recovery runbook drilled
post-1.0
all users
- TODO User-requested features triaged via feedback loop
- TODO Cross-product ecosystem integration (friend's prices/strategy site)
- TODO Tax framing + locale-aware capital gains math
- TODO Mobile native apps (if web tells us they're needed)
- TODO Family / household tracking (multi-user portfolios)
- TODO AI advisor / coaching (opt-in)
Ship log (most recent)
// The page previously only read the manually-set annual_income field (null for every broker import) — so it showed nothing despite 164 dividend + 108 withholding + 384 interest events sitting in the ledger since Dec 2023. Rebuilt on three legs: RECEIVED — real broker history aggregated by year (dividends / withholding / interest / net) + a 12-month payout bar strip; SAME BOOK, PAST DECADE — hypothetical reconstruction, today's share counts × each year's actual split-adjusted per-share payouts (new yahooDividendHistory via chart events=div, GBp-normalized) at today's FX; EXPECTED — trailing-twelve-month per-share payouts × current shares per holding, with per-holding table (shares, TTM/share, income, yield, HIST·TTM vs MANUAL source) and manually-set annual_income rows folded in. New lib/dividends.ts; events amounts were never encrypted so received numbers are exact. Workers-budget aware (≤14 ticker lookups, biggest positions first).
// A Trading 212 import 'doing nothing' unravelled into the real story: since the 2026-06-01 plaintext-column drop, 11 query sites still requested dropped columns. PostgREST 400s the whole query when any selected column doesn't exist, and every caller discarded the error object — so every ledger surface silently read []: net worth €0, rank stuck at L1, assets list empty, import dedup blind (re-uploads duplicated all positions), snapshot capture 400ing on WRITE (9-day history gap), weekly digest selecting dead columns. Purged the lot: ledger-reads ASSET/LIABILITY_COLS (the root), tier.ts (incl. a never-existed liabilities.current_balance — liabilities now actually count against net worth), both snapshot readers + the capture upsert, three events.ts reconciliation reads, import dedup, digest cron (net_worth_enc + per-user decrypt). Verified against live PostgREST: old shapes 400, new shapes 200. Data repair: 171 duplicate assets from the blind-dedup window deleted (the 36 originals intact); business login renamed hello@legacycodex.xyz → hello@millefold.com in auth + profiles (Google sub-matching had kept the pre-rebrand email). Rank ladder rethemed to the dynasty line: Apprentice → Steward → Mercenary → Merchant → Magnate → Patrician → Sovereign (slugs/L-codes stable).
// Verifying the sandbox surfaced a deep one: the daily price cron had NEVER updated a row — Stooq deployed an anti-bot proof-of-work wall that blocks datacenter IPs, both price tables sat empty since creation, and every HIST rate silently degraded to class assumptions. Yahoo Finance chart API now fronts quotes + 10y daily history (stocks and crypto quotes; GBp pence normalized ÷100 — the GBX trap — with 429-backoff retries and request spacing); Stooq and CoinGecko demoted to fallbacks. Second root cause: the Workers free plan caps a request at 50 subrequests, which deterministically killed sweeps after ~13 tickers. refreshMarketPrices now batches writes (2 upserts total), threads an explicit fetch budget, reuses previously-resolved Yahoo symbols (1 fetch/ticker steady-state), processes unknowns first with a per-ticker spend cap, and defers leftovers to the next run. End state: 32/35 portfolio tickers priced with history, budget no longer exhausted; 3 dead broker codes (13M/RY6/ML) permanently fall back to assumptions by design. Engine + lookup verified end-to-end from prod egress; scripts/verify-sandbox.mts added as the repeatable check.
// Second founder-cohort feature, same day. /dashboard/sandbox builds hypothetical portfolios out of slices — your real holdings (carrying their HIST/ASSUMED rates from the personal-projection engine), asset classes at stated assumptions, live ticker lookups (real 10y price CAGR fetched via server action), or custom rates. Weight inputs with sum indicator + normalize-to-100. CURRENT vs ALT A vs ALT B race on one chart, end-state table shows final value and delta vs current at the chosen horizon, same monthly DCA into every path. Entitlement gate shared with PERSONAL (founder-cohort accounts; other accounts see a locked page). Shared math extracted to lib/projection-math.ts; CLASS_ASSUMPTIONS split into client-safe lib/class-assumptions.ts. SANDBOX added to dashboard nav, open_sandbox link on projection. Also: cursor lightning-on-click effect removed (CursorLightning → slim CursorDot, −300 lines).
// The first founder-cohort feature ships. New lib/personal-projection.ts blends one nominal rate from the user's actual mix: broker-imported tickers get a real 10y price CAGR (Stooq dailies cached in market_price_history, price-only ex-dividends, min 1y span), everything else uses stated per-class assumptions (stocks 8 / crypto 15 / savings 2.5 / real estate 4 / vehicles −8 …), weighted by FX-converted values. Projection page gets a full-width PERSONAL preset + a holding-by-holding breakdown table showing weight, rate, and HIST·xY vs ASSUMED source per row, with history-coverage % in the panel head. Gating via new lib/entitlements.ts: founder-cohort accounts get it; everyone else sees a locked preset. CLASS_ASSUMPTIONS doubles as the growth table for per-category forward projection. Also: next 16.2.6 → 16.2.7 (closes audit item L8).
// Five buttons across the projection panel: MEAN (5%) / HISTORICAL (7%, default) / GENEROUS (10%) / CUSTOM (any %) / PERSONAL (soon). Each has bold label + mute one-liner explaining the rate. PERSONAL is a coming-soon placeholder: weighted CAGR from the user's actual holdings. Roadmap updated; custom-portfolio sandbox added as an alpha 4 todo.
// New /dashboard/history page: single-date backfill form (pick a past date + total assets + total liabilities → upserts a snapshot) + bulk CSV import (date,total_assets,total_liabilities OR date,net_worth, up to 1000 rows). Existing same-date rows overwrite. Snapshots get id surfaced so individual rows are deletable. ProjectionPreview rewritten as a curve chart: horizon slider 5-40y, decade gridlines, monthly contribution + return inputs, milestone cards at +10/+20/+30/+40y. Per-category filter parked as schema decision (~2d).
// Email/recovery: callback returns specific error codes (expired_link / link_used / invalid_token / oauth_cancelled / missing_code), login page surfaces them with friendly copy, check-email state shows the entered address + 30s resend cooldown + 'lost access' escape to /contact. Activity feed: new activities table + SECURITY DEFINER get_activity_feed() function + /dashboard/activity page showing my level-ups + OG claims + follows + followees' public events. Snapshot insights: chart now has 7d/30d/90d/all range toggle (loads up to 365d), milestones-crossed list (€1k–€10M thresholds), biggest-swing callout in-range. All three alpha 4 items closed.
// Phase plan rewritten into alpha 1-4 → beta 1-3 → pre-release → 1.0 → post-1.0. Donate button slotted into alpha 4 (vendor decision parked, Ko-fi recommended). Mission Control gets a seed-demo-ledger / clear-demo-rows panel (10 assets across all 9 categories + 3 liabilities + 60d snapshots tagged [DEMO] / source=demo_seed) so the redesigned dashboard renders with shape. Scanner-probe WAF rule confirmed actively blocking in the wild.
// All six high-priority Phase 4 secrets shipped and verified: BREVO_API_KEY (email live), MISSIONCONTROL_PASSWORD + SECRET (24h HMAC gate), SUPABASE_SERVICE_ROLE_KEY (deletion + cron + webhook unlock), NEXT_PUBLIC_VAPID_PUBLIC_KEY + VAPID_PRIVATE_KEY (push), CRON_SECRET (Wrangler + GitHub). GH Actions cron-digest.yml fires weekly. Cloudflare WAF: 10/min rate-limit on POST / + /login (1 free slot) + custom rule blocking scanner probes (wp-/.env/.git/xmlrpc/etc + empty UA). hello@millefold.com also granted is_admin alongside paulius485@gmail.com.
// Removed `export const runtime = 'edge'` from /api/cron/digest and replaced lib/email.ts import with inline Brevo fetch. The edge declaration + transitive server-only import combo returned 500 before reaching the handler. Test workflow run returns 200.
// join_waitlist() lets anon insert + receive unsubscribe_token without needing SELECT on the table (anon had no SELECT, so the previous .insert().select() chain 401'd). sendEmail now awaited so Cloudflare Workers doesn't terminate the dangling promise mid-flight.