Concept: anonymous visitors earn visibility by doing: completing lessons/exercises unlocks the ability to see more content, ratio-style (complete N → the next M become visible). Creating an account removes the ratio entirely — everything is fully unlocked for authenticated users. The gate is an on-ramp/conversion mechanic, never a paywall for account holders.
Current state (verified): sandbox mode is anonymous-open, but lesson mode hard-redirects to login (language-runner.ts auth gate) and every progress endpoint in LessonsController is [Authorize] (/progress, /{lessonNumber}/attempt, /progress/batch, /complete-tutorial, /submit). There is no entitlement/unlock concept anywhere in the platform — existing gating is prerequisites + roles only. So anonymous lesson play itself is new ground, not just the ratio.
Questions the spike must answer:
Anonymous identity — device-scoped token vs signed local completion ledger. Prior art: the progress-sync IndexedDB outbox (libraries/platform/progress-sync/) already queues completions offline for later verification; evaluate extending it with a server-issued anonymous id
Trust/verification — which endpoints get anonymous variants; how server-side grading verifies anonymous completions (attempt tokens like /{lessonNumber}/attempt but unauthenticated); abuse resistance (rate limits, replay, farming)
The ratio itself — starting free allotment per language; N completions → M unlocked; per-language vs global pool; interaction with existing prerequisite locks (ratio gates visibility, prerequisites gate order)
UX — how locked-but-earnable lessons render in the sidebar (distinct from prerequisite locks); the "create an account to unlock everything" moment (celebratory, not naggy); what happens to earned anonymous progress on signup (migrate the ledger into UserLessonProgress)
Content-layer interaction — do books (#326)/text (#327)/videos (#328) participate in the ratio or stay free as enticement? Recommendation to be made by the spike
Deliverable: design doc comment on this issue + implementation sub-tickets. No production code in the spike.
Spike under epic #323.
**Concept:** anonymous visitors earn visibility by doing: completing lessons/exercises unlocks the ability to see more content, ratio-style (complete N → the next M become visible). Creating an account removes the ratio entirely — **everything is fully unlocked for authenticated users**. The gate is an on-ramp/conversion mechanic, never a paywall for account holders.
**Contribution currency (decided):** lesson/exercise completions.
**Current state (verified):** sandbox mode is anonymous-open, but lesson mode hard-redirects to login (`language-runner.ts` auth gate) and every progress endpoint in `LessonsController` is `[Authorize]` (`/progress`, `/{lessonNumber}/attempt`, `/progress/batch`, `/complete-tutorial`, `/submit`). There is no entitlement/unlock concept anywhere in the platform — existing gating is prerequisites + roles only. So anonymous lesson play itself is new ground, not just the ratio.
**Questions the spike must answer:**
- [ ] **Anonymous identity** — device-scoped token vs signed local completion ledger. Prior art: the `progress-sync` IndexedDB outbox (`libraries/platform/progress-sync/`) already queues completions offline for later verification; evaluate extending it with a server-issued anonymous id
- [ ] **Trust/verification** — which endpoints get anonymous variants; how server-side grading verifies anonymous completions (attempt tokens like `/{lessonNumber}/attempt` but unauthenticated); abuse resistance (rate limits, replay, farming)
- [ ] **The ratio itself** — starting free allotment per language; N completions → M unlocked; per-language vs global pool; interaction with existing prerequisite locks (ratio gates *visibility*, prerequisites gate *order*)
- [ ] **UX** — how locked-but-earnable lessons render in the sidebar (distinct from prerequisite locks); the "create an account to unlock everything" moment (celebratory, not naggy); what happens to earned anonymous progress on signup (migrate the ledger into `UserLessonProgress`)
- [ ] **Content-layer interaction** — do books (#326)/text (#327)/videos (#328) participate in the ratio or stay free as enticement? Recommendation to be made by the spike
- [ ] **Telemetry** — funnel events for anonymous → contributing → account conversion
**Deliverable:** design doc comment on this issue + implementation sub-tickets. No production code in the spike.
Design: completion-ratio unlocks for anonymous visitors
Spike deliverable. Grounded in the current code (auth gate in language-runner.ts, [Authorize] on all progress endpoints, attempt-token flow, progress-sync IDB outbox, RedisRateLimitMiddleware). Implementation sub-tickets follow in the next comment.
Principles (locked in at epic filing)
Currency = lesson/exercise completions. 2. Account = everything unlocked, immediately and forever. 3. The ratio is an on-ramp, never a paywall.
What "see more" means (clarified)
GET /api/Lessons is already anonymous — the catalog listing is public today. What's gated is playing lessons (attempt/submit/complete are [Authorize], and the SPA hard-redirects lesson mode to login). So the ratio gates playability, not listing: every lesson title stays visible in the sidebar (motivating), but beyond-horizon lessons are ratio-locked. Books/text/videos content layers (#326/#327/#328) stay free as enticement — they advertise the depth an account gets.
POST /api/Lessons/anonymous/session (anonymous, tightly rate-limited): issues an opaque anonId — GUID + server-side record with a 90-day idle TTL (Mongo TTL index). Client stores it in localStorage beside the existing sandbox state.
Rejected: pure client-side ledger (trivially forged; the whole point is earned server-verified visibility) and device fingerprinting (privacy-hostile, unreliable). Prior art to reuse: the progress-sync IDB outbox already queues completions for later server verification — the anon path is that machinery pointed at anon endpoints.
Privacy posture: no PII, no cross-device linking, explicitly not an account; expiring the id just resets the on-ramp.
2. Trust & verification — anonymous variants of the existing grading path
GET /api/Lessons/{n}/attempt/anonymous + POST /api/Lessons/{n}/submit/anonymous carrying the anonId: same strategy-generated per-attempt content, same server-side grading (or CodeExecution-worker re-verification like /progress/batch). Nothing about grading gets weaker for anon users — the attempt token already defeats copy-paste.
New Mongo collection anonymousLessonProgress keyed (anonId, lessonNumber) with TTL matching the session — UserLessonProgress shape minus identity.
Abuse resistance: the anon endpoints only serve lessons inside the current horizon (server-checked, not client-checked); Redis rate limits per anonId AND per IP on session-create + submit; daily anon completion cap (e.g. 20/day) — generous for a human, boring for a farmer. Completion farming earns nothing valuable anyway: the unlock is visibility, not certificates or skills (skills stay account-only, which is itself a conversion hook).
3. The ratio
Free allotment: Tier 0 + Tier 1 per language (≈ first 10–14 lessons — welcome + foundations). Enough to genuinely learn something before the first gate.
Formula:visibleCount = freeAllotment + floor(anonCompletions × 1.5), per language, over the display-number ordering. Complete 2 → see 3 more. The multiplier > 1 means the horizon accelerates — momentum feels rewarded, and an engaged visitor effectively never hits a wall until deep in the curriculum, by which point they've invested enough to want the account.
Prerequisites still gate order inside the horizon (existing isLessonUnlocked); the ratio gates the horizon itself. Two different lock semantics, two different icons (below).
Server is the source of truth: horizon computed backend-side from anonymousLessonProgress, returned by a tiny GET /api/Lessons/anonymous/horizon?language= (or piggybacked on the session-validate response).
4. Sign-in — the conversion moment
On first authenticated session with an anonId present: POST /api/Lessons/progress/claim migrates the anon ledger into UserLessonProgress (idempotent, then deletes the anon records) and triggers the existing skill auto-award check — your anonymous work retroactively earns the badges. That's the celebratory hook: "Signed in — 3 lessons claimed, Bronze C# badge earned."
UX: ratio-locked rows get a distinct icon + tooltip ("Complete 2 more lessons to reveal — or sign in and see everything"); each horizon-advance fires a small celebration snackbar; a quiet persistent footer line in the sidebar carries the sign-in CTA. Never a modal, never a nag — the product's tone is old-school generous.
5. Telemetry funnel
Existing activity-tracking domains, four events: anon-session-created → anon-lesson-completed → unlock-milestone → signup-with-claim. That's the whole funnel; Seq/Grafana can chart conversion without new infrastructure.
Explicitly out of scope for v1
Cross-device anon continuity, anon leaderboards, gating the content layers, any change to sandbox mode (stays fully open), and skills for anonymous users (deliberate conversion incentive).
# Design: completion-ratio unlocks for anonymous visitors
Spike deliverable. Grounded in the current code (auth gate in `language-runner.ts`, `[Authorize]` on all progress endpoints, attempt-token flow, `progress-sync` IDB outbox, `RedisRateLimitMiddleware`). Implementation sub-tickets follow in the next comment.
## Principles (locked in at epic filing)
1. Currency = **lesson/exercise completions**. 2. Account = **everything unlocked**, immediately and forever. 3. The ratio is an on-ramp, never a paywall.
## What "see more" means (clarified)
`GET /api/Lessons` is already anonymous — the catalog *listing* is public today. What's gated is **playing** lessons (attempt/submit/complete are `[Authorize]`, and the SPA hard-redirects lesson mode to login). So the ratio gates **playability**, not listing: every lesson title stays visible in the sidebar (motivating), but beyond-horizon lessons are ratio-locked. Books/text/videos content layers (#326/#327/#328) stay **free** as enticement — they advertise the depth an account gets.
## 1. Anonymous identity — server-issued anonymous session (recommended)
- `POST /api/Lessons/anonymous/session` (anonymous, tightly rate-limited): issues an opaque `anonId` — GUID + server-side record with a **90-day idle TTL** (Mongo TTL index). Client stores it in `localStorage` beside the existing sandbox state.
- **Rejected:** pure client-side ledger (trivially forged; the whole point is *earned* server-verified visibility) and device fingerprinting (privacy-hostile, unreliable). Prior art to reuse: the `progress-sync` IDB outbox already queues completions for later server verification — the anon path is that machinery pointed at anon endpoints.
- Privacy posture: no PII, no cross-device linking, explicitly not an account; expiring the id just resets the on-ramp.
## 2. Trust & verification — anonymous variants of the existing grading path
- `GET /api/Lessons/{n}/attempt/anonymous` + `POST /api/Lessons/{n}/submit/anonymous` carrying the anonId: same strategy-generated per-attempt content, same server-side grading (or CodeExecution-worker re-verification like `/progress/batch`). **Nothing about grading gets weaker for anon users** — the attempt token already defeats copy-paste.
- New Mongo collection `anonymousLessonProgress` keyed `(anonId, lessonNumber)` with TTL matching the session — `UserLessonProgress` shape minus identity.
- **Abuse resistance:** the anon endpoints only serve lessons *inside the current horizon* (server-checked, not client-checked); Redis rate limits per anonId AND per IP on session-create + submit; daily anon completion cap (e.g. 20/day) — generous for a human, boring for a farmer. Completion farming earns nothing valuable anyway: the unlock is visibility, not certificates or skills (skills stay account-only, which is itself a conversion hook).
## 3. The ratio
- **Free allotment:** Tier 0 + Tier 1 per language (≈ first 10–14 lessons — welcome + foundations). Enough to genuinely learn something before the first gate.
- **Formula:** `visibleCount = freeAllotment + floor(anonCompletions × 1.5)`, per language, over the display-number ordering. Complete 2 → see 3 more. The multiplier > 1 means the horizon accelerates — momentum feels rewarded, and an engaged visitor effectively never hits a wall until deep in the curriculum, by which point they've invested enough to want the account.
- **Prerequisites still gate order** inside the horizon (existing `isLessonUnlocked`); the ratio gates the horizon itself. Two different lock semantics, two different icons (below).
- Server is the source of truth: horizon computed backend-side from `anonymousLessonProgress`, returned by a tiny `GET /api/Lessons/anonymous/horizon?language=` (or piggybacked on the session-validate response).
## 4. Sign-in — the conversion moment
- On first authenticated session with an anonId present: `POST /api/Lessons/progress/claim` migrates the anon ledger into `UserLessonProgress` (idempotent, then deletes the anon records) and triggers the existing skill auto-award check — **your anonymous work retroactively earns the badges**. That's the celebratory hook: "Signed in — 3 lessons claimed, Bronze C# badge earned."
- UX: ratio-locked rows get a distinct icon + tooltip ("Complete 2 more lessons to reveal — or sign in and see everything"); each horizon-advance fires a small celebration snackbar; a quiet persistent footer line in the sidebar carries the sign-in CTA. **Never a modal, never a nag** — the product's tone is old-school generous.
## 5. Telemetry funnel
Existing activity-tracking domains, four events: `anon-session-created` → `anon-lesson-completed` → `unlock-milestone` → `signup-with-claim`. That's the whole funnel; Seq/Grafana can chart conversion without new infrastructure.
## Explicitly out of scope for v1
Cross-device anon continuity, anon leaderboards, gating the content layers, any change to sandbox mode (stays fully open), and skills for anonymous users (deliberate conversion incentive).
Spike deliverable complete: design doc above + tickets. Two decisions surfaced for your sign-off before implementation starts (defaults chosen in the doc, cheap to change now, annoying later):
The ratio — free Tiers 0–1, then floor(completions × 1.5) horizon growth. Steeper (×1) converts harder; shallower (×2) converts softer.
Skills stay account-only — anonymous completions earn visibility but badges only arrive via the sign-in claim. This is the main conversion hook; flag if you'd rather anon users earn badges live.
Leaving this spike open for your review of the design; close it if the direction looks right, or comment adjustments and I'll fold them into #331/#332.
Implementation sub-tickets filed: **#331** (backend — anon sessions, graded anon progress, horizon computation, sign-in claim) and **#332** (frontend — anon lesson mode, horizon lock UI, claim celebration, funnel events; depends on #331).
Spike deliverable complete: design doc above + tickets. Two decisions surfaced for your sign-off before implementation starts (defaults chosen in the doc, cheap to change now, annoying later):
1. **The ratio** — free Tiers 0–1, then `floor(completions × 1.5)` horizon growth. Steeper (×1) converts harder; shallower (×2) converts softer.
2. **Skills stay account-only** — anonymous completions earn visibility but badges only arrive via the sign-in claim. This is the main conversion hook; flag if you'd rather anon users earn badges live.
Leaving this spike open for your review of the design; close it if the direction looks right, or comment adjustments and I'll fold them into #331/#332.
Spike delivered and fully implemented. Design doc approved; implementation shipped via #331 (backend, PR #56) and #332 (frontend, PRs #99 + #100), all merged to master. Closing the spike.
Spike delivered and fully implemented. Design doc approved; implementation shipped via #331 (backend, PR #56) and #332 (frontend, PRs #99 + #100), all merged to `master`. Closing the spike.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Spike under epic #323.
Concept: anonymous visitors earn visibility by doing: completing lessons/exercises unlocks the ability to see more content, ratio-style (complete N → the next M become visible). Creating an account removes the ratio entirely — everything is fully unlocked for authenticated users. The gate is an on-ramp/conversion mechanic, never a paywall for account holders.
Contribution currency (decided): lesson/exercise completions.
Current state (verified): sandbox mode is anonymous-open, but lesson mode hard-redirects to login (
language-runner.tsauth gate) and every progress endpoint inLessonsControlleris[Authorize](/progress,/{lessonNumber}/attempt,/progress/batch,/complete-tutorial,/submit). There is no entitlement/unlock concept anywhere in the platform — existing gating is prerequisites + roles only. So anonymous lesson play itself is new ground, not just the ratio.Questions the spike must answer:
progress-syncIndexedDB outbox (libraries/platform/progress-sync/) already queues completions offline for later verification; evaluate extending it with a server-issued anonymous id/{lessonNumber}/attemptbut unauthenticated); abuse resistance (rate limits, replay, farming)UserLessonProgress)Deliverable: design doc comment on this issue + implementation sub-tickets. No production code in the spike.
Design: completion-ratio unlocks for anonymous visitors
Spike deliverable. Grounded in the current code (auth gate in
language-runner.ts,[Authorize]on all progress endpoints, attempt-token flow,progress-syncIDB outbox,RedisRateLimitMiddleware). Implementation sub-tickets follow in the next comment.Principles (locked in at epic filing)
What "see more" means (clarified)
GET /api/Lessonsis already anonymous — the catalog listing is public today. What's gated is playing lessons (attempt/submit/complete are[Authorize], and the SPA hard-redirects lesson mode to login). So the ratio gates playability, not listing: every lesson title stays visible in the sidebar (motivating), but beyond-horizon lessons are ratio-locked. Books/text/videos content layers (#326/#327/#328) stay free as enticement — they advertise the depth an account gets.1. Anonymous identity — server-issued anonymous session (recommended)
POST /api/Lessons/anonymous/session(anonymous, tightly rate-limited): issues an opaqueanonId— GUID + server-side record with a 90-day idle TTL (Mongo TTL index). Client stores it inlocalStoragebeside the existing sandbox state.progress-syncIDB outbox already queues completions for later server verification — the anon path is that machinery pointed at anon endpoints.2. Trust & verification — anonymous variants of the existing grading path
GET /api/Lessons/{n}/attempt/anonymous+POST /api/Lessons/{n}/submit/anonymouscarrying the anonId: same strategy-generated per-attempt content, same server-side grading (or CodeExecution-worker re-verification like/progress/batch). Nothing about grading gets weaker for anon users — the attempt token already defeats copy-paste.anonymousLessonProgresskeyed(anonId, lessonNumber)with TTL matching the session —UserLessonProgressshape minus identity.3. The ratio
visibleCount = freeAllotment + floor(anonCompletions × 1.5), per language, over the display-number ordering. Complete 2 → see 3 more. The multiplier > 1 means the horizon accelerates — momentum feels rewarded, and an engaged visitor effectively never hits a wall until deep in the curriculum, by which point they've invested enough to want the account.isLessonUnlocked); the ratio gates the horizon itself. Two different lock semantics, two different icons (below).anonymousLessonProgress, returned by a tinyGET /api/Lessons/anonymous/horizon?language=(or piggybacked on the session-validate response).4. Sign-in — the conversion moment
POST /api/Lessons/progress/claimmigrates the anon ledger intoUserLessonProgress(idempotent, then deletes the anon records) and triggers the existing skill auto-award check — your anonymous work retroactively earns the badges. That's the celebratory hook: "Signed in — 3 lessons claimed, Bronze C# badge earned."5. Telemetry funnel
Existing activity-tracking domains, four events:
anon-session-created→anon-lesson-completed→unlock-milestone→signup-with-claim. That's the whole funnel; Seq/Grafana can chart conversion without new infrastructure.Explicitly out of scope for v1
Cross-device anon continuity, anon leaderboards, gating the content layers, any change to sandbox mode (stays fully open), and skills for anonymous users (deliberate conversion incentive).
Implementation sub-tickets filed: #331 (backend — anon sessions, graded anon progress, horizon computation, sign-in claim) and #332 (frontend — anon lesson mode, horizon lock UI, claim celebration, funnel events; depends on #331).
Spike deliverable complete: design doc above + tickets. Two decisions surfaced for your sign-off before implementation starts (defaults chosen in the doc, cheap to change now, annoying later):
floor(completions × 1.5)horizon growth. Steeper (×1) converts harder; shallower (×2) converts softer.Leaving this spike open for your review of the design; close it if the direction looks right, or comment adjustments and I'll fold them into #331/#332.
Spike delivered and fully implemented. Design doc approved; implementation shipped via #331 (backend, PR #56) and #332 (frontend, PRs #99 + #100), all merged to
master. Closing the spike.