|
| 1 | +# Post-MVP backlog |
| 2 | + |
| 3 | +Live list of work that is intentionally *not* in MVP. Originally lived |
| 4 | +inside `milestones/00_initial_design_doc.md` §1, but the design doc is a |
| 5 | +frozen historical artifact and this list keeps growing — so it lives |
| 6 | +here now. |
| 7 | + |
| 8 | +Each entry is "what + why-it's-deferred + rough shape if obvious." |
| 9 | +Nothing here is committed; this is the queue we pull from when deciding |
| 10 | +what to build next, not a roadmap with dates. |
| 11 | + |
| 12 | +## Scope expansion |
| 13 | + |
| 14 | +- PLL, F2L, and other solve-stage expansion. |
| 15 | +- Other cube types (4×4, Megaminx, etc.). |
| 16 | +- Public case browser (no login required). |
| 17 | + |
| 18 | +## Product surface |
| 19 | + |
| 20 | +- Stats over time and progress graphs (a placeholder/skeleton view |
| 21 | + ships in MVP so users know it's coming). |
| 22 | +- Admin panel. |
| 23 | +- Dark mode. |
| 24 | +- Additional public-facing marketing pages (features, pricing, FAQ, |
| 25 | + etc.) beyond the M5 landing page. |
| 26 | +- Full accessibility audit — screen-reader walkthrough, ARIA live |
| 27 | + regions, complete WCAG AA review. M5 ships a basic pass: keyboard |
| 28 | + nav, focus rings, form labels, spot-check contrast. |
| 29 | +- **Avatar.** Allow users to pick an OLL case as their avatar in the |
| 30 | + style of the logo. |
| 31 | + |
| 32 | +## Study mechanics |
| 33 | + |
| 34 | +- **Per-user timezone + local-midnight rollover** for streak/due-date |
| 35 | + comparisons. MVP uses server UTC date for "today" — a user in PST |
| 36 | + sees streaks tick at 5 PM Pacific (00:00 UTC). Post-MVP: store |
| 37 | + `users.timezone`, roll over at user-local midnight. Two reviews near |
| 38 | + UTC midnight currently can fall on different "today" values; that |
| 39 | + goes away with per-user rollover. |
| 40 | +- **Free-study filters: disable chips with no remaining matches.** |
| 41 | + Today, picking "L" as the primary shape leaves every tag chip enabled |
| 42 | + even though tags like "knight_move" don't intersect with L cases — |
| 43 | + the user discovers this only by hitting "0 cases match". Post-MVP: |
| 44 | + gray out / hide chips whose addition would leave the result set |
| 45 | + empty given the current filter state. Same treatment for tags/state |
| 46 | + interactions. |
| 47 | + |
| 48 | +## Account / data |
| 49 | + |
| 50 | +- **"Download my data" / data export before account deletion.** |
| 51 | + Deferred from M7 (`milestones/07_delete_account.md`). MVP delete is |
| 52 | + straight hard-delete; a JSON dump endpoint + Settings-side download |
| 53 | + flow can layer in once explicit user demand surfaces. |
| 54 | +- **Guest mode "Discard guest data" Settings entry.** Deferred from M7. |
| 55 | + Trivially `clearGuestState()`, but the UX (confirmation pane, warning |
| 56 | + copy) deserves its own design pass — the existing M7 deletion flow |
| 57 | + is account-scoped and doesn't apply. |
| 58 | + |
| 59 | +## Email / notifications |
| 60 | + |
| 61 | +- **Email reminders.** Opt-in daily/weekly nudge when the user has |
| 62 | + cards due. Needs a `users.reminder_preference` enum (off / daily / |
| 63 | + weekly), a per-user "last reminder sent" timestamp, and a worker pass |
| 64 | + that runs on a schedule. Resend integration already in place from |
| 65 | + M1 — wiring is the easy part; the design call is cadence, copy, and |
| 66 | + unsubscribe-link semantics. |
| 67 | +- **Easier-to-copy verification / reset codes in email.** Today the |
| 68 | + 6-digit code sits inside an HTML paragraph; on mobile the user has to |
| 69 | + long-press and trim whitespace. Wrap the code in a `<code>`/monospace |
| 70 | + box with generous padding so a single tap selects the whole code, or |
| 71 | + include a Markdown-style fenced block in the plaintext email body. |
| 72 | + Trivial template change once the design lands. |
| 73 | + |
| 74 | +## Monetization |
| 75 | + |
| 76 | +- **Paid email subscription / premium tier.** Possible monetization |
| 77 | + path. Would need: subscription state on `users`, a billing provider |
| 78 | + (Stripe likely), gated features (TBD — possibly stats over time, |
| 79 | + additional puzzle types, or dark mode), and a customer-portal flow |
| 80 | + for plan changes / cancellations. Discovery work; not committed. |
| 81 | + |
| 82 | +## Infrastructure / ops |
| 83 | + |
| 84 | +- **Cold-start UX safety net.** Render free tier spins down after ~15 |
| 85 | + min idle; the next request stalls 20–60s while the dyno wakes. The |
| 86 | + splash screen handles the boot case, but in-session API calls |
| 87 | + (grade, reveal, etc.) currently look frozen. Two layered ideas: |
| 88 | + 1. **Top progress bar (NProgress-style).** ~2px bar at the top of |
| 89 | + the viewport, animates while any axios request is in flight. |
| 90 | + Axios interceptors increment/decrement a counter; bar component |
| 91 | + watches it. Calibrated so it's invisible on a 200ms request and |
| 92 | + visibly creeps on a 30s one. Zero visual cost on fast requests, |
| 93 | + immediate signal on slow ones. |
| 94 | + 2. **Subtle full-screen overlay after ~8 seconds.** When a request |
| 95 | + has been in flight that long, fade in a 30–50% black overlay |
| 96 | + with a small note: "still working — the server takes up to a |
| 97 | + minute to wake up after a quiet period." Dismisses on response. |
| 98 | + Threshold tuned to fire effectively only on cold-start, not on |
| 99 | + slow mobile networks. |
| 100 | + |
| 101 | + Either of these makes the spin-down tolerable. The "real fix" is a |
| 102 | + paid Render tier (~$5/mo) once the project has any monetization or |
| 103 | + donation path. |
| 104 | + |
| 105 | +## Privacy / compliance |
| 106 | + |
| 107 | +A grab-bag of ways to tighten the app's privacy posture and make the |
| 108 | +forthcoming Privacy Policy easier to defend. Not all of these will get |
| 109 | +done — they're noted so the decision to skip any one of them is |
| 110 | +deliberate rather than accidental. |
| 111 | + |
| 112 | +- **Self-host the WOFF2 font files instead of loading from Google |
| 113 | + Fonts.** Today `frontend/src/assets/tokens.css` imports Newsreader, |
| 114 | + Inter Tight, and JetBrains Mono from `fonts.googleapis.com`. That |
| 115 | + means Google sees every visitor's IP on every page load, which |
| 116 | + contradicts the "we don't share data with Google beyond reCAPTCHA" |
| 117 | + posture in the privacy policy. Fix is one afternoon of work: |
| 118 | + download the WOFF2s, drop them under `frontend/public/fonts/`, |
| 119 | + replace the `@import` with `@font-face` blocks pointing at the local |
| 120 | + paths. SIL OFL allows redistribution; no attribution required beyond |
| 121 | + the license file itself. Bonus: faster first paint, since the |
| 122 | + browser stops doing a cross-origin DNS lookup + handshake for |
| 123 | + fonts.googleapis.com on every cold visit. |
| 124 | +- **Replace Google reCAPTCHA v3 with Cloudflare Turnstile.** Today |
| 125 | + registration sends a token derived from the user's browser |
| 126 | + fingerprint + IP to Google. Turnstile is Cloudflare's equivalent |
| 127 | + with a no-tracking design (no third-party cookies, no behavioral |
| 128 | + fingerprinting marketed back to ads) and we already proxy through |
| 129 | + Cloudflare anyway, so the subprocessor count actually drops by one. |
| 130 | + Drop-in swap on the registration form + one env var on the backend. |
| 131 | +- **Hash the email in `account_deletions` instead of storing |
| 132 | + plaintext.** Audit table currently stores `email` + `deleted_at` so |
| 133 | + we can support re-registration limits and abuse investigation. A |
| 134 | + one-way hash (with a server-side pepper) covers both use cases — |
| 135 | + re-register flow hashes the candidate email and looks for a match — |
| 136 | + without keeping recognizable PII after the user asks to be deleted. |
| 137 | + The downside is we lose human-readable rows for abuse review; the |
| 138 | + upside is the audit table stops being a deletion-survives-deletion |
| 139 | + problem. |
| 140 | +- **Add error tracking thoughtfully (self-hosted GlitchTip or Sentry) |
| 141 | + or commit publicly to not adding it.** Right now we have neither, |
| 142 | + which means real production errors disappear silently. Path A: |
| 143 | + self-host GlitchTip on a $5 box so error payloads never leave our |
| 144 | + infra; we'd need to scrub user emails / IDs from breadcrumbs before |
| 145 | + ingest. Path B: write "no error tracking is wired up" into the |
| 146 | + privacy policy and live with the operational cost. Either is |
| 147 | + defensible; the current "vaguely meaning to add Sentry" middle state |
| 148 | + is not. |
| 149 | +- **Reduce session cookie lifetime / add idle expiry.** Current JWT is |
| 150 | + a 30-day absolute lifetime with no server-side idle timeout. Options: |
| 151 | + drop absolute lifetime to 7 days, add a 14-day idle timeout enforced |
| 152 | + on the server (`sessions.last_seen_at`, refreshed lazily on each |
| 153 | + authenticated request), or rolling expiry that re-issues the JWT on |
| 154 | + active use. Trade-off is "log me back in" friction vs. the size of |
| 155 | + the steal-cookie blast radius. |
| 156 | +- **`SECURITY.md` + responsible-disclosure email.** Standard hygiene |
| 157 | + for any public-facing app. A `SECURITY.md` at the repo root pointing |
| 158 | + at a `security@nebulouscode.com` (or similar) tells researchers where |
| 159 | + to send a quiet heads-up rather than opening a public GitHub issue. |
| 160 | + Coupled with a one-line acknowledgement section that names them once |
| 161 | + the patch ships, this is one of the cheapest professionalism wins. |
| 162 | +- **`/.well-known/privacy.txt` and/or Global Privacy Control signal.** |
| 163 | + Two adjacent ideas: ship a `privacy.txt` at a well-known path |
| 164 | + pointing at the policy, contact, and jurisdiction, *and/or* honor |
| 165 | + the `Sec-GPC: 1` browser header by treating it as a do-not-sell / |
| 166 | + do-not-share signal at the API layer. We don't sell data today so |
| 167 | + GPC is mostly a posture statement, but it's also future-proofing |
| 168 | + against ever adding analytics. |
| 169 | +- **Switch transactional email off Resend.** Resend is fine but it's |
| 170 | + another US-based subprocessor that sees every user's email address + |
| 171 | + the contents of verification/reset emails. Options: a self-hosted |
| 172 | + MTA (Postal, Maddy, or even raw Postfix on a $5 box), or a more |
| 173 | + privacy-aligned provider like Postmark or Tutanota's transactional |
| 174 | + product. The self-host path is meaningfully more work (DNS, DKIM, |
| 175 | + SPF, DMARC, deliverability monitoring) and probably only worth it |
| 176 | + alongside the EU residency work below. |
| 177 | +- **Stop using JWTs for sessions; use opaque session tokens.** JWTs are |
| 178 | + fine in our shape (we already track a `sessions` row with a token |
| 179 | + hash so sign-out-all works), but the JWT itself is structurally a |
| 180 | + bearer credential that's hard to revoke server-side without the |
| 181 | + parallel sessions table. Switching to opaque random tokens (just the |
| 182 | + `sessions.token_hash` UUID, validated by DB lookup on each request) |
| 183 | + collapses the design — one source of truth for "is this session |
| 184 | + alive", no JWT-secret-rotation problem, no `exp`/`iat` claim drift. |
| 185 | + Cost is one DB hit per authenticated request (already happening for |
| 186 | + user lookup anyway). |
| 187 | +- **Region-pinned data residency option.** Only worth doing if we |
| 188 | + start to pursue EU users seriously. Would mean: an EU-region Neon |
| 189 | + branch, an EU-region Render service, EU-aligned email subprocessor, |
| 190 | + and a routing layer (Cloudflare Workers or similar) that pins a |
| 191 | + signed-up EU user's traffic to the EU stack. Big project; flagged |
| 192 | + here so future-us knows the cost of saying "we serve the EU" out |
| 193 | + loud. |
| 194 | + |
| 195 | +## Testing |
| 196 | + |
| 197 | +- **HTTP integration tests for route handlers.** Today we unit-test |
| 198 | + the lib functions each handler delegates to (high coverage on the |
| 199 | + business logic) but not the HTTP plumbing — auth gating, request |
| 200 | + validation, JSON response shape, status codes, cookie attachment. |
| 201 | + The handler-layer files (`routes/*`, `auth/extractor.rs`, |
| 202 | + `auth/session.rs`, `error.rs`'s `IntoResponse`) are excluded from the |
| 203 | + 95% coverage gate for that reason. Adding tower-based in-process |
| 204 | + integration tests (spin up the Axum app, send real `Request`s via |
| 205 | + `tower::ServiceExt::oneshot`, assert responses) would close the gap. |
| 206 | + Cost: ~30–50% more test code, slower CI; benefit: catches |
| 207 | + handler-level regressions (renamed JSON fields, dropped auth gates, |
| 208 | + wrong status codes) that lib-level tests can't see. Worth doing once |
| 209 | + the project has more than one developer, or on the first regression |
| 210 | + that the lib tests miss. |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## Out of scope (MVP) |
| 215 | + |
| 216 | +These were called out at design time as explicitly *not* the direction |
| 217 | +of the product, not just "not yet": |
| 218 | + |
| 219 | +- Native mobile app. |
| 220 | +- Social features. |
| 221 | +- Sharing custom decks between users. |
0 commit comments