Pattern 001. Shelf A
The streak lesson
One small lesson a day, a counter that goes up when you show up, and tomorrow’s lesson already in view. It is the most copied loop in learning apps, and the easiest one to build badly.
The loop
- promptYou open it, because you decided to
- actionA two-minute lesson with a visible end
- rewardTwo new things you can do, and the count goes up by one
- returnTomorrow's lesson is already named on the home screen
A streak lesson is three parts that depend on each other. The lesson is small enough that “I have no time” is not true. The streak turns a row of small days into one number you can see. The next step is shown, so coming back needs no decision.
Take away the lesson and you have a counter that counts nothing. Take away the counter and day nine feels the same as day one. Take away the next step and people finish a lesson, feel good, and never think of it again.
Why it holds attention
Three mechanisms, each with evidence behind it:
- A chain you do not want to break. A study of seven experiments found that an unbroken streak raises the chance someone does the activity again, and that a break discourages people most when they blame themselves and least when the streak can be repaired (Silverman and Barasch, Journal of Consumer Research, 2023).
- Speeding up near the finish. People put in more effort as a goal gets closer (Kivetz, Urminsky and Zheng, 2006). “Lesson 4 of 13” does this work for you. An endless course does not.
- Ability. A behaviour happens when motivation, ability and a prompt arrive together (the Fogg Behavior Model). You cannot control motivation. You can make the lesson two minutes long.
The largest language-learning app publishes how much this loop matters to it: its former product lead wrote that the share of daily users on a streak of seven days or more nearly tripled, to over half, in the years its growth returned. The same essay warns that people with long streaks were always going to be the keen ones, so do not read a streak as the cause of everything.
The version you would defend
A streak can be a kindness or a threat. The difference is what happens on the day someone misses.
Build these in from the first commit:
- The lesson ends. After today’s lesson the home screen says so and offers nothing that counts. Practice is there for people who want it, and it is labelled “does not count”.
- A missed day is forgiven. Dit gives two rest days. One missed day spends a rest day instead of the streak. One comes back every seven days.
- A broken streak takes nothing else away. The message is “the streak starts again today; the 6 letters you know are still yours.” What was learned is the real asset. The number is a way of seeing it.
- No reminders until asked for. The starter sends none. If you add them, the person turns them on, picks the time, and one tap turns them off.
- Leaving is easy. Copy your data, erase your data, both on the first settings screen.
Run the regret test on your own build: if the person could see everything you know about why each screen is there, would they still do the lesson? For a two-minute lesson with a visible end, yes.
Data model
One record, kept on the device.
| Field | Meaning |
|---|---|
streak, best |
current and longest run of days |
lastDone |
the last day a lesson was finished, as YYYY-MM-DD |
rests |
rest days in hand, 0 to 2 |
lesson |
index of the next lesson |
known |
what has been learned so far |
sound |
a preference |
Dates are stored as calendar days in the person’s own time zone, not as timestamps. A streak is about days as that person lives them. Storing a timestamp and comparing against a server’s midnight is the most common streak bug there is.
Screens
- Home. The count, what the count means right now, today’s lesson, what you know. Empty state: “None yet. The first lesson takes about two minutes.”
- Lesson. One question at a time, a progress bar, “3 of 8”. A way out at the top.
- Done. What you gained, the streak, one button back. No upsell here.
- Settings. Sound, rest days explained, copy my data, erase my data.
Build it with an assistant
Give the assistant one slice at a time and run each before asking for the next. These prompts are written to be pasted.
Make a single-page web app in three files: index.html, style.css, app.js. No libraries, no build step, no network requests. It teaches [YOUR SUBJECT] in 13 short lessons of two items each. For now build only the data: a LESSONS list and a function buildLesson(state, day) that returns a list of steps (show, pick one of four, produce the answer). Use a seeded random generator so the same day always gives the same steps. Show me the steps for day one as JSON in the page.Add the state object {streak, best, lastDone, rests, lesson, known} saved to localStorage under one key. Write a pure function finishDay(state, day) that takes dates as "YYYY-MM-DD" strings: same day does nothing; next day adds one; a gap is covered by rest days if there are enough, otherwise the streak restarts at 1; every seventh day gives one rest day back, up to 2. Do not use the clock inside this function. Then write six test cases for it as plain asserts in the console.Add a today() function that returns the local calendar day, and that returns the value of a ?day=YYYY-MM-DD address parameter when one is present. Build the home screen, the lesson screen and the done screen. After a lesson is finished today, the home screen must not offer another one.Add a settings dialog: a sound switch, "Copy my data" and "Erase my data" (two taps, no confirm pop-up). Then check the page at 360 px wide for sideways scroll, check every button is at least 44 px tall, and check keyboard focus is visible.Read what comes back. If you cannot say what finishDay does on a three-day gap with one rest day left, you do not have an app yet, you have a file.
The starter, live
Dit
Morse code, two letters a day
Try this
- Do today’s lesson, then look at the home screen: there is no second lesson to do.
- Add ?day=2026-03-04 to the address to visit another day, and skip one: a rest day covers it.
- Skip ten days and read what the app says.
- Open Settings and erase everything in two taps.
The whole streak rule is this function. It never looks at the clock, which is why it can be tested:
// The streak rule, and the part that makes it one you can defend:
// a missed day spends a rest day instead of wiping the count, and a real break
// restarts the streak without taking away anything you learned.
function finishDay(state, day) {
if (state.lastDone === day) return { changed: false, usedRests: 0, restarted: false };
const gap = state.lastDone ? daysBetween(state.lastDone, day) : 1;
const missed = Math.max(0, gap - 1);
let usedRests = 0, restarted = false;
if (missed === 0) state.streak += 1;
else if (missed <= state.rests) { state.rests -= missed; usedRests = missed; state.streak += 1; }
else { state.streak = 1; restarted = true; }
if (state.streak % 7 === 0 && state.rests < MAX_RESTS) state.rests += 1; // earn one back each week
state.best = Math.max(state.best, state.streak);
state.lastDone = day;
return { changed: true, usedRests: usedRests, restarted: restarted };
}
// What the home screen should say about the streak right now, before any lesson.
function streakStatus(state, day) {
if (!state.lastDone) return { shown: 0, note: "A streak counts the days you did a lesson." };
const gap = daysBetween(state.lastDone, day);
if (gap <= 0) return { shown: state.streak, note: "Done for today. Best so far: " + state.best + "." };
if (gap === 1) return { shown: state.streak, note: "Today's lesson keeps it going." };
if (gap - 1 <= state.rests) return { shown: state.streak, note: "You missed " + (gap - 1) + (gap === 2 ? " day" : " days") + ". A rest day covers it if you do today's lesson." };
return { shown: 0, note: "Welcome back. The streak starts again today; the " + state.known.length + " letters you know are still yours." };
}And this is what makes a day repeatable. The same day always produces the same questions:
// A small random number generator with a seed, so a given day always asks the same
// questions. That makes the app testable, and nobody can reload until the questions get easy.
function rng(seedText) {
let h = 2166136261;
for (const ch of seedText) { h ^= ch.charCodeAt(0); h = Math.imul(h, 16777619); }
return function () { h ^= h << 13; h ^= h >>> 17; h ^= h << 5; return ((h >>> 0) % 100000) / 100000; };
}
function shuffle(list, rand) {
const a = list.slice();
for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
return a;
}
function buildLesson(state, day, practice) {
const rand = rng(day + (practice ? "p" : "l") + state.lesson);
const fresh2 = practice || state.lesson >= LESSONS.length ? [] : LESSONS[state.lesson].split("");
const pool = Array.from(new Set(state.known.concat(fresh2)));
const steps = fresh2.map((L) => ({ kind: "show", letter: L }));
const quizzed = shuffle(fresh2.concat(shuffle(pool, rand).slice(0, 2)), rand).slice(0, practice ? 5 : 4);
quizzed.forEach((L) => {
const others = shuffle(Object.keys(CODE).filter((x) => x !== L), rand).slice(0, 3);
steps.push({ kind: "pick", letter: L, options: shuffle(others.concat(L), rand) });
});
shuffle(pool, rand).slice(0, 2).forEach((L) => steps.push({ kind: "key", letter: L }));
return steps;
}Make it yours
Shipping Dit as it stands would be shipping this guide’s example. Change at least these five things:
- The subject. Replace
CODEandLESSONS. Knots, chords, sign-language letters, wine regions, the Greek alphabet, your own course. - The name and the mark. Not a pun on a well-known app’s name. Search both stores first.
- The look. New palette, new type, your own drawing. The lamp is Dit’s; find yours in your subject.
- One rule. Three lessons a week instead of daily. A shared streak for two people. A streak counted in weeks.
- One screen. A map of the course. A certificate at the end. A “teach it back” step.
The legal lines
| You may copy | You may not copy | Check first |
|---|---|---|
| Daily lessons, streak counters, rest days, progress bars, leagues: these are methods, and copyright does not protect a “procedure, process, system, method of operation” (17 U.S.C. 102(b)) | Another app’s course text, audio, illustrations, mascot, sounds, or the particular look of its screens | Any name with a famous app’s word in it. Apple: “You cannot use another developer’s icon, brand, or product name in your app’s icon or name” (guideline 4.1(c)) |
| Facts and public-domain material as lesson content (Morse code, the periodic table, scales) | A textbook’s wording or the selection and order of a published course | The licence of any word list, audio or image set you did not make |
| The idea of a friendly character | A specific character | Whether children will use it: under 13 in the US brings COPPA, and reminders and streaks aimed at children are named in the UK and EU children’s design rules |
Education, not legal advice. The full framework and every source are in the chapter “What you may copy”.
Ship it
- Where it sells. Education and self-improvement, as a subscription or a one-time purchase of the full course.
- The honest version of the paywall. The first lessons free, the price shown in real money before any billing detail is asked for, and cancelling as easy as subscribing (the US law is ROSCA, 15 U.S.C. 8403).
- The base rate. Across subscription apps the median app earns about $72 a month a year after launch, and about one in six reaches $1,000 a month within two years (RevenueCat, State of Subscription Apps 2026). A streak does not change those odds. A subject people already want to learn, and a way of reaching them, does.
- First hundred users. Go where people already study your subject and give the first three lessons away as a link that works in a browser. That is why the starter is a web page.
Sources
- Silverman and Barasch, “On or Off Track: How (Broken) Streaks Affect Consumer Decisions”, Journal of Consumer Research, 2023. Summary read 2026-11-10: https://www.colorado.edu/business/faculty-research/2023/04/19/or-track-how-broken-streaks-affect-consumer-decisions
- Kivetz, Urminsky and Zheng, “The Goal-Gradient Hypothesis Resurrected”, Journal of Marketing Research, 2006. Summary read 2026-11-10: https://business.columbia.edu/faculty/research/goal-gradient-hypothesis-resurrected-purchase-acceleration-illusionary-goal
- Fogg Behavior Model, read 2026-11-10: https://www.behaviormodel.org/
- Jorge Mazal, “How Duolingo reignited user growth”, read 2026-11-10: https://www.lennysnewsletter.com/p/how-duolingo-reignited-user-growth
- 17 U.S.C. 102(b), read 2026-11-10: https://www.law.cornell.edu/uscode/text/17/102
- App Review Guidelines (last updated 16 October 2026), read 2026-11-10: https://developer.apple.com/app-store/review/guidelines/
- 15 U.S.C. 8403, read 2026-11-10: https://www.law.cornell.edu/uscode/text/15/8403
- RevenueCat, State of Subscription Apps 2026, read 2026-11-10: https://www.revenuecat.com/state-of-subscription-apps/