# Vibe Coding 101 — full catalogue
Source: https://vibe-coding-101.brighteng.org
Terms: 277
License signal: search=yes, ai-train=yes, ai-input=yes
## loop
How you work. TDD, reverse engineering, skeletons, stranglers. The model writes the code. You choose the method.
### 01 test-driven development
id: tdd
url: https://vibe-coding-101.brighteng.org/c/tdd/
one_liner: Write a failing test first, then make the implementation pass. The test is the spec. The code is fill.
also: the outer loop of red-green-refactor
what: TDD is three beats: red (the test fails, so it is a real test), green (the smallest code that passes), refactor (clean up under a green bar). It is not “write more tests.” It is “the test arrives before the design.”
why_vibe: Models are completers. A prose brief gets you a neighbouring implementation. A failing test gives the model an objective function: turn red into green. Runnable, repeatable, hostile to hallucination.
move: Write or dictate one example: input, output, failure message. Have the model write only the test. You run it and confirm red. New turn: “Production code only. Make this test green. Do not edit the test.” Refactor after green.
tell_the_model: Write one failing test for (example). No implementation. I will confirm red, then you may edit production code only. Do not edit the test to make it pass.
pitfall: Asking the model to “write the tests too” in the same turn. It will write green tests that assert the code it just invented. That is a prize-giving, not a driver.
### 02 reverse engineering
id: reverse-engineering
url: https://vibe-coding-101.brighteng.org/c/reverse-engineering/
one_liner: Recover a spec from a finished thing: behaviour, data, edges. Not copying pixels — extracting a testable contract.
also: taking a living system apart
what: Reverse engineering is recovering what the software actually does when the docs are missing or untrustworthy. The output is examples, a state machine, a data shape — not a review.
why_vibe: Everyone says “make it like X.” Fed only that sentence, the model does a skin. Reverse engineering turns “like X” into clauses: what the button does on an empty list, where a failed payment returns, whether changing an id in the URL leaks a row.
move: Pick a living product. List 8–12 user actions as input → visible result. Screenshot if you can. Lock those actions with characterization tests or a checklist. Only then let the model rebuild, inside that fence — not from a homepage screenshot and vibes.
tell_the_model: Reverse-engineer (product). Do not copy the look. List user actions as input → result. Output a behaviour list and the data objects. Wait for my OK before writing code.
pitfall: Treating reverse engineering as “summarise the repo.” A summary is not a contract. Without failing examples, the next turn drifts.
### 03 characterization test
id: characterization-test
url: https://vibe-coding-101.brighteng.org/c/characterization-test/
one_liner: Snapshot what the system does today, then change it. Not the ideal — the actual.
also: golden master
what: A characterization test records current behaviour, even when that behaviour is wrong. Its job is to scream when you move. Snapshots, golden files, recorded HTTP conversations.
why_vibe: The model’s favourite crime in a legacy tree is a helpful rewrite. Characterization lets you say: behaviour frozen, structure may move. A later turn is allowed to change behaviour, on purpose.
move: Record input and output on the critical paths. Commit them as tests. Tell the model those snapshots must stay green. Spec changes get their own turn.
tell_the_model: Add characterization tests for (paths) that lock current input/output. Do not edit those tests during the refactor. Behaviour changes are a separate request.
pitfall: Snapshots so wide that every edit goes red, and someone deletes the suite. Keep them narrow: only the paths you actually care about.
### 04 spike
id: spike
url: https://vibe-coding-101.brighteng.org/c/spike/
one_liner: Code you intend to throw away, written only to answer one unknown. Stop when the clock hits.
what: A spike is time-boxed exploration: can this API stream, can this model read a PDF, can the database enforce the permission. The deliverable is knowledge, not a feature.
why_vibe: Vibe coding turns spikes into products because the model never stops. You have to say “two hours, throw it away, one question,” or the repo keeps a barely-running thing with no tests.
move: A branch of its own. Write the single question. Stop on time. File the answer in prose or an ADR. Delete the branch. The real implementation starts clean, holding that answer.
tell_the_model: This is a spike. Throw it away in two hours. Answer only: (question). No abstractions, no suite, no cleanup. Stop on time. Three sentences of conclusion.
pitfall: “We’ll tidy it after it works,” and the tidying never comes. A spike merged to main is not a spike.
### 05 walking skeleton
id: walking-skeleton
url: https://vibe-coding-101.brighteng.org/c/walking-skeleton/
one_liner: A path so thin it is almost bone, but it walks end to end: deploy, sign in, write one row, read it back.
what: A walking skeleton wires the real pipes first — build, environment, auth, storage, one round trip. Features come later. It proves the system walks, not that a screen looks finished.
why_vibe: The model loves settings screens and dark mode. A skeleton reverses the order: one live record through an environment shaped like production. TDD has somewhere to hang after that.
move: Name the thinnest path, e.g. sign up → create one record → still there after refresh. No flanking features. Deploy a preview. Grow the second piece only when that path is green.
tell_the_model: Walking skeleton first: sign up, create one (object), still there after refresh. No settings, no filters, no dark mode. Get that path running in preview.
pitfall: A skeleton made of fake data and fake auth. That is a poster. It cannot walk.
### 06 tracer bullet
id: tracer-bullet
url: https://vibe-coding-101.brighteng.org/c/tracer-bullet/
one_liner: One live round: a small feature through every layer, leaving a visible trace so you can correct aim.
what: Close to a walking skeleton, but the point is a real feature, real data, real integrations — extremely narrow. You fire it to see the trajectory, not to finish the war.
why_vibe: The model works in layers: finish the whole data tier, then the whole UI. A tracer forbids that. One thin slice through the stack, so you correct the prompt against a real interface.
move: Pick one user-visible win, e.g. “paste a URL, save a bookmark.” Punch through UI, API, storage. See which layer’s assumption was wrong. Fire the next round.
tell_the_model: One tracer bullet: the user can complete (one thing), through UI, API, and database. Do not finish any layer. I must be able to click it in preview.
pitfall: Using “tracer” as a synonym for skeleton, wiring everything and shipping no user action. The round has to hit a user verb.
### 07 vertical slice
id: vertical-slice
url: https://vibe-coding-101.brighteng.org/c/vertical-slice/
one_liner: Ship one user-visible sliver at a time, instead of finishing a layer.
what: A vertical slice is cut on user value, not on technical layers. Each slice contains the bit of UI, logic, and data that slice needs.
why_vibe: “Design the database first” is the model’s default. Slices give you something to accept every turn. Without them, a vibe-coding thread generates half a system at once.
move: Write the feature as a list of user verbs. One verb per turn. Finish, test, merge, then take the next.
tell_the_model: This turn is one vertical slice: (user verb). No other verbs. Tests included. I must be able to click it.
pitfall: Cutting a slice called “perfect auth.” Auth is platform, not the first user value — unless the product is auth.
### 08 refactoring
id: refactoring
url: https://vibe-coding-101.brighteng.org/c/refactoring/
one_liner: Change structure, keep behaviour. An edit with no tests is not a refactor. It is a rewrite.
what: Refactoring is design under tests: rename, extract, move a boundary. Observable behaviour stays.
why_vibe: The model hears “tidy this” as “invent a feature.” Split refactoring from behaviour change, and name the tests that must not move.
move: Bar is green. Name the structure to change. No new behaviour. Run the same tests. The diff should move code, not grow new branches.
tell_the_model: Refactor (scope) only. Behaviour frozen. Tests frozen. No features on the side. The diff must not grow new business branches.
pitfall: “Refactor, and fix that bug while you’re in there.” The model does both, and you cannot tell which edit broke the build.
### 09 strangler fig
id: strangler-fig
url: https://vibe-coding-101.brighteng.org/c/strangler-fig/
one_liner: Grow a new skin around the old system, take it over piece by piece, let the old wood die. No big-bang rewrite.
what: A strangler fig wraps the old system: routing sends some requests to new code, and the old code is retired in pieces.
why_vibe: The model offers to rewrite the directory. On legacy code that is a disaster. A strangler replaces one entry at a time, with characterization tests holding the rest.
move: Pick one entry — a route, a button. Put a switch in front, pointing at the new implementation. Run both. Shift traffic. Delete the old. Pick the next entry.
tell_the_model: Strangle (entry). Do not rewrite the module. Add a switch so old and new both run. Tests lock old behaviour.
pitfall: A dual-write switch that never comes down. Old and new both live forever. The fig does not strangle. It tangles.
## spec
How you pin the thing down. Types, scenarios, a noun list. A mood is not a spec.
### 10 spec-driven
id: spec-driven
url: https://vibe-coding-101.brighteng.org/c/spec-driven/
one_liner: A checkable spec before generated code. Shorter than the chat, harder than a vibe.
what: Spec-driven work writes “what” as something a machine and a human can mark right or wrong: tests, types, OpenAPI, a state table, a checklist. Code is one solution of that spec.
why_vibe: Threads drift. Fifty turns in, nobody remembers the first sentence. The spec is the page nailed to the repo. The model reads that, not your mood.
move: Open a file: objects, fields, actions, failing examples. Pin the examples with tests or types. The prompt says “implement this spec.” New rules go into the spec first, not into the chat.
tell_the_model: Write the spec file, not the feature. Objects, fields, actions, two failing examples. I will edit the spec. Then implement. The file is source of truth. Do not invent rules.
pitfall: A spec written as marketing. “An elegant experience” cannot go red. A spec has to be able to fail.
### 11 types as spec
id: types-as-spec
url: https://vibe-coding-101.brighteng.org/c/types-as-spec/
one_liner: Write the data shape as types first, and let the compiler shout at the model.
what: Types are a machine-readable spec: fields, unions, impossible states. If the types fail, the implementation is not done.
why_vibe: Models write JavaScript eagerly, and invent fields. Locking types first is a free “did you make something up” check every turn. Cheaper than scanning the diff by eye.
move: Commit the types first. Ban `any` as an escape. Run tsc. Fix from the errors. Do not edit the types to soothe a wrong implementation.
tell_the_model: Types first, no logic. Fields for (object) are as follows. No `any`. Not done until tsc is clean. Do not widen types to make a bad implementation compile.
pitfall: Everything is `string` and optional. That is an empty spec. Encode impossible states as unions, or the types have no teeth.
### 12 behaviour-driven development
id: bdd
url: https://vibe-coding-101.brighteng.org/c/bdd/
one_liner: Specify with scenarios: given, when, then. A human can read them. A test can run them.
also: Given-When-Then
what: BDD writes requirements as examples. Given is setup, When is the action, Then is an observable result. Behaviour, not implementation.
why_vibe: “Make checkout nice” cannot land. Three Given-When-Then rows can. Outsiders can write scenarios. That is the format to use when you cannot name the class.
move: Three scenarios per feature, including one failure. The model turns them into tests. You confirm red, then implement.
tell_the_model: Write three Given-When-Then scenarios for (feature), one of them a failure path. Turn them into tests. No implementation yet.
pitfall: A Then that says “the user feels delighted.” That is not an observation. Then has to be readable on a screen or in a response.
### 13 ubiquitous language
id: ubiquitous-language
url: https://vibe-coding-101.brighteng.org/c/ubiquitous-language/
one_liner: The same word for the same thing in the chat, the code, and the tests. The noun list is the spec.
what: Ubiquitous language comes from domain-driven design: the team shares one noun set, and the code uses that set instead of a more “technical” dialect.
why_vibe: This is the actual choke point in vibe coding. If you can say “debounce,” one sentence is enough. If you can only say “don’t spin while I type,” the model guesses among neighbours. The language is not style. It is compression.
move: Before a feature, list objects and verbs: task, owner, mark done. The prompt uses only those words. If the model renames them, rename the code back. Do not adopt its dialect.
tell_the_model: Use only these nouns: (list). Code, tests, and UI copy use them. Do not invent synonyms. If the tree already uses other words, rename to this list.
pitfall: A noun list that lives only in chat. Next week’s thread invents another. The list has to live in the repo.
### 14 YAGNI
id: yagni
url: https://vibe-coding-101.brighteng.org/c/yagni/
one_liner: You aren’t gonna need it. Do not let the model build the abstraction nobody asked for.
what: YAGNI is Extreme Programming’s brake: only what this slice needs. Plugin systems, theme engines, generic frameworks — not unless this slice uses them.
why_vibe: The model’s prior is “complete product.” Without YAGNI it builds settings, roles, and dark mode. In vibe coding YAGNI belongs in every prompt, not on a poster.
move: Each turn, list three things not to do. That list matters more than the feature list. If the model opens a new abstraction, stop, delete, start the turn again.
tell_the_model: YAGNI. No notifications, no themes, no plugins, no roles. Only this slice: (verb). Delete extra files.
pitfall: Using YAGNI to skip tests. Tests belong to this slice. What you skip is imaginary tomorrow.
### 15 minimum viable product
id: mvp
url: https://vibe-coding-101.brighteng.org/c/mvp/
one_liner: The smallest version that tests one hypothesis — not a complete product with fewer buttons.
what: An MVP has one question to learn. The feature set serves that question. After you learn, you grow. It is not a roadmap cut in half.
why_vibe: You say “keep it simple,” and the model still builds half a SaaS. Write the hypothesis: “Will someone do (verb) three days in a row?” The smallest software that answers that is the MVP.
move: Write the hypothesis. List the actions required to see the answer. Everything else is YAGNI. Walking skeleton first, then those actions.
tell_the_model: This MVP tests only: (hypothesis). Build only (actions). Do not shrink a full product. Build the hypothesis.
pitfall: An MVP that is a demo reel: visible, not usable. Learning needs real data and a real user verb, not slides.
### 16 contract-first
id: contract-first
url: https://vibe-coding-101.brighteng.org/c/contract-first/
one_liner: Freeze the interface — types, OpenAPI, events — then fill both sides.
what: Contract-first freezes the boundary: request shape, error codes, event names. Both sides implement against one file.
why_vibe: When the model writes both sides at once, it invents two dialects. Commit the contract first and both sides become fill-in-the-blanks. Drift is caught by types or contract tests.
move: Write OpenAPI or shared types first. Generate or hand-write the client. Implement the server to satisfy it. No silent fields in the implementation.
tell_the_model: Contract-first. Write (OpenAPI / shared types) before any page. Freeze fields and error codes. Implementation must obey. No second JSON dialect.
pitfall: A contract nobody runs. Without a consumer test it is another document that rots.
## agent
How the model runs, and how you stop it. Context, tools, review, evals.
### 17 vibe coding
id: vibe-coding
url: https://vibe-coding-101.brighteng.org/c/vibe-coding/
one_liner: Drive a model to edit the repo in natural language. You review the diff, run the tests, name the move — syntax is not the first job.
what: Vibe coding is a way of working: the human owns direction and acceptance, the model writes most of the code. It is not “you don’t need to know programming.” It moves what you know from syntax to spec, tests, and review.
why_vibe: The word gets used as an excuse. Smooth vibe coding looks like TDD, reverse engineering, slicing — with an agent on the keyboard. Unsmooth vibe coding is an unnamed wish and one generation.
move: Each turn, name one move. Add constraints. Run tests. Read the diff. Do not spike and ship in the same turn.
tell_the_model: This is a vibe-coding turn. The move is (TDD / reverse engineering / a slice). Constraints below. Plan first, wait for OK, then edit. Run tests after.
pitfall: Accepting “feels right.” A neighbouring implementation that feels right is exactly what the term was coined to avoid.
### 18 prompt as spec
id: prompt-as-spec
url: https://vibe-coding-101.brighteng.org/c/prompt-as-spec/
one_liner: Write the prompt as a ticket: objects, constraints, acceptance, what not to do. Not a mood.
what: A good prompt is a ticket, not a muse. Short, reusable, and when it fails you can point at the line that was ignored.
why_vibe: “Make it nicer” cannot go red. Treat the prompt as spec, and a good turn can be saved as the next turn’s opening.
move: Four blocks: goal, constraints, acceptance, forbidden. Acceptance is a command or a test. When a turn works, commit that prompt into the repo.
tell_the_model: Follow the ticket. Goal: … Constraints: … Acceptance: (command) must pass. Forbidden: … Do not embellish.
pitfall: Pasting the whole previous thread as the spec. Noise drowns the four blocks. Excerpt, do not dump.
### 19 context engineering
id: context-engineering
url: https://vibe-coding-101.brighteng.org/c/context-engineering/
one_liner: Control what the model can see right now. Only the papers this turn needs stay on the table.
what: Context is not memory. It is a window. Files, errors, and chat all take space. The engineering is selection, clipping, pinning.
why_vibe: When the window is full of irrelevant files, the model edits the wrong one, confidently. Failed vibe coding is often failed context, not a stupid model.
move: Open two or three relevant files per turn. Paste the full error. Do not pour the repository into the thread. When the thread is long, start another and drop the spec file in again.
tell_the_model: Look only at these files: (list). Do not read others. The full error is source of truth. Do not edit closed files from memory.
pitfall: “Add the whole repo for completeness.” Completeness here is dilution.
### 20 tool use
id: tool-use
url: https://vibe-coding-101.brighteng.org/c/tool-use/
one_liner: The model does not only talk — it runs commands, edits files, clicks a browser. Every step must be stoppable.
what: Tool use plugs the agent into a real environment: shell, editor, browser, tests. Power goes up. So does the blast radius.
why_vibe: Without tools the model can only emit a patch. With tools it can run the red bar itself. You name which tools it may use, and “plan before touching.”
move: Default: plan first. Limit writable paths. You name the test command. No opportunistic repo-wide format, no git push --force.
tell_the_model: You may run (test command) and edit (paths). Plan first, wait for OK. Do not format the repository, force-push, or touch unrelated files.
pitfall: Handing an agent an unrestricted shell and leaving the room. Tools want a whitelist.
### 21 human in the loop
id: human-in-the-loop
url: https://vibe-coding-101.brighteng.org/c/human-in-the-loop/
one_liner: The model proposes, the human nods, then it moves. Irreversible steps halt.
what: Human-in-the-loop puts the human on the gates that matter: the plan, deletes, migrations, spend, publish. Not on every keystroke.
why_vibe: Full auto feels great until it refactors half the tree. In the loop means: generation can be fast. Merge stays slow.
move: Two gates: plan, merge. The plan gate checks the steps. The merge gate checks the diff and the tests. Let it run in between.
tell_the_model: Plan first. No file edits. After I agree, you may edit. Stop before merge until I have read the diff.
pitfall: A loop that asks at every step, until the human mash-approves. Few gates, hard gates.
### 22 evals
id: evals
url: https://vibe-coding-101.brighteng.org/c/evals/
one_liner: Score the prompt on a fixed task set. A vibe is not a regression suite.
what: Evals are tests for model behaviour: the same inputs, the same scoring — asserts, diffs, spot checks. Changing a prompt is like changing code. You watch the eval bar.
why_vibe: One prompt edit can bend the whole agent chain. Without evals you remember “it felt better.” With evals you see whether that sentence broke reverse-engineering.
move: Collect ten real tasks, including failures. Make them a script. Prompt edits run those ten. One drop, roll that sentence back.
tell_the_model: Do not edit the prompt by feel. Add one eval: (input) → (required result). Run the whole eval set before the next sentence.
pitfall: Evals that are all happy paths. The model will study for the test. Put failures in, or the suite is theatre.
### 23 guardrails
id: guardrails
url: https://vibe-coding-101.brighteng.org/c/guardrails/
one_liner: What the agent must not do, as mechanical limits: paths, commands, permissions, spend.
what: Guardrails are hard limits in the environment, not manners in the prompt. Sandbox, CI, code owners.
why_vibe: “Please don’t drop production” is not a guardrail. A read replica is. The model forgets. The environment does not.
move: List disasters: drop database, force push, edit secrets, format the tree. Block each with tool permissions or CI, not with a postscript in the prompt.
tell_the_model: Guardrails: only (paths); no secrets, no force push, no production migrations. Enforce with permissions, not with prompt text alone.
pitfall: Guardrails that live only in a system prompt, gone when you switch agents. They have to travel with the repo.
### 24 diff review
id: diff-review
url: https://vibe-coding-101.brighteng.org/c/diff-review/
one_liner: The human job is the diff, not the file. If you have not seen what moved, you do not merge.
what: Diff review reads the contrast: new branches, deleted tests, drive-by formatting. Cheaper than the whole file, harder to fool with “it runs.”
why_vibe: Vibe coding turns the human from author into editor. Editors do not rewrite the book. They read the change. Skip the diff and you gave the merge key to the model.
move: Three checks per diff: did scope escape this slice, did tests get weaker, did unrelated files move. One yes, send it back.
tell_the_model: After edits, summarise the diff: which files, why. No drive-by formatting. Changes outside (scope) go in another turn.
pitfall: Checking only that tests are green. Green can mean an assertion was deleted. A missing test in the diff is worse than a red bar.
## feedback
How you know this round is right. Red tests, CI, fakes, a repro, one e2e. Eyeballing is not enough.
### 25 red-green-refactor
id: red-green-refactor
url: https://vibe-coding-101.brighteng.org/c/red-green-refactor/
one_liner: TDD’s metronome: red, then green, then tidy. Skipping a beat is a bet.
what: Red proves the test can fail. Green proves the implementation is enough. Refactor proves design can move under green. Drop one beat and the loop is broken.
why_vibe: The model skips red and writes a green test. You need to see red with your own eyes. Without red you do not know the test is a test.
move: Force three turns, or three pauses in one turn: you run red, you run green, you read the refactor diff. Do not let the model play all three beats in one breath.
tell_the_model: Strict red-green-refactor. This turn is red only: write the failing test, stop. I will run it. No implementation.
pitfall: New behaviour smuggled into the refactor beat. That is a secret green, not a refactor.
### 26 CI as judge
id: ci-as-judge
url: https://vibe-coding-101.brighteng.org/c/ci-as-judge/
one_liner: The merge key belongs to the pipeline, not to a vibe. Local green is not court. CI is.
what: CI runs tests, types, and the build in a clean environment. It does not trust your node_modules, or the model saying “I ran it.”
why_vibe: Agents see green in dirty environments. CI is the unsentimental employee. Vibe coding without CI is a courtroom with no judge.
move: One command for typecheck, test, build. Required on the PR. The model’s “done” means CI green. Local green is not done.
tell_the_model: Done means CI green, not that you say you ran it. After edits, run (command). On failure, fix from the errors. Do not disable checks.
pitfall: Skipping a red CI. That fires the judge. Fix the check, or fix the code.
### 27 linter loop
id: linter-loop
url: https://vibe-coding-101.brighteng.org/c/linter-loop/
one_liner: Treat lint and type errors as the model’s compiler. The red text is spec, not insult.
what: The linter loop: edit → run lint/tsc → paste the raw output back → edit again. Until clean. Do not paraphrase the error.
why_vibe: The model eats raw errors better than your paraphrase. Your job is to run the command, not to explain TypeScript.
move: One command. On failure, paste the whole thing. No disabling a rule to “make it pass.”
tell_the_model: Fix from this error. Do not disable the rule. No `any`. Raw output:
pitfall: Pasting only the first line. The “caused by” below is often the root. Paste all of it.
### 28 snapshot test
id: snapshot-test
url: https://vibe-coding-101.brighteng.org/c/snapshot-test/
one_liner: Pin output to a file. If it changes, red. Good for characterization. Bad at asserting intent.
what: A snapshot test archives serialised output and diffs it next run. It is good at “did something unexpected move,” bad at “is this right.”
why_vibe: Models fidget with copy and DOM structure. Snapshots yell. If the model may update snapshots itself, it is grading its own exam. Humans own the update.
move: Snapshots on critical render paths. The prompt says: no -u. If a snapshot must move, you run it, you read the diff.
tell_the_model: Add snapshots that lock output for (paths). Do not update snapshots this turn. If they go red, stop and show me the diff.
pitfall: A megalith snapshot that goes red on CSS whitespace. Keep them small and aimed at behaviour.
### 29 property-based testing
id: property-based
url: https://vibe-coding-101.brighteng.org/c/property-based/
one_liner: Do not only write examples. Write a property that must always hold, and let a generator try to break it.
what: Property tests declare invariants: round-trip encoding, sort preserves length, a permission function never leaks on random ids. The generator hunts counterexamples.
why_vibe: The model studies for the two examples you typed. Property tests face it with inputs it has not seen. For parsing, permissions, and money, that is harsher than one more example.
move: Write the invariant. Use a library to generate inputs. On failure, keep the shrunk counterexample as a plain test.
tell_the_model: Property tests for (function). Invariant: (property). Not just examples. If you find a counterexample, keep it as a fixed test.
pitfall: An invariant that only says “does not crash.” Too weak. Write “round-trips” or “never returns another user’s row.”
### 51 repro
id: repro
url: https://vibe-coding-101.brighteng.org/c/repro/
one_liner: Steps that make the bug happen on a clean machine. Without a repro, the model is guessing.
what: A repro is a short, ordered list: start here, click this, see that. Better: a failing test. Best: a command that exits non-zero.
why_vibe: “It’s broken” plus a screenshot of the whole desktop is how you burn five turns. A repro lets the model run, fail, patch, run. That is TDD with a bug as the spec.
move: Before asking for a fix, write the repro. If you cannot, that is the first task: make it fail on command. Then the model may touch code.
tell_the_model: Do not guess. Here is the repro: (steps or command). Make a failing test from it first, then fix. If you cannot reproduce, stop and say so.
pitfall: A repro that needs your laptop, your cookies, and a full moon. Reduce it. If it needs prod data, snapshot a fixture.
### 52 end-to-end test
id: e2e
url: https://vibe-coding-101.brighteng.org/c/e2e/
one_liner: A script that drives the real UI through a real user path. Slow, few, precious.
also: e2e
what: E2E tests click the product like a user: browser, network, database. They catch wiring the unit tests cannot see. They are expensive and flaky if you have too many.
why_vibe: Models generate 40 e2e files for every button. You wanted three: sign up, the money path, the permission denial. Name e2e and name the budget.
move: Cap e2e at the paths that lose money or leak data. Everything else is a unit or integration test. Run e2e on CI, not on every save.
tell_the_model: Add at most one e2e for (user path). Do not e2e every button. Unit-test the rest. The e2e must not depend on wall-clock sleeps; wait for the UI.
pitfall: `sleep(5000)` as a wait. That is how e2e becomes flaky. Wait for the element, not the clock.
### 53 flaky test
id: flaky-test
url: https://vibe-coding-101.brighteng.org/c/flaky-test/
one_liner: A test that fails without a product bug: time, order, network, leftover state. Delete or fix. Never retry to green.
what: Flakes are non-determinism in the suite. Retrying them in CI launders the failure. The suite stops being a judge.
why_vibe: Models “fix” flakes by raising timeouts or catching all errors. That hides the race. You wanted the race named and killed.
move: Quarantine a flake in the open. Next turn: make it deterministic or delete it. Do not add `retries: 3` as the solution.
tell_the_model: This test is flaky. Do not raise timeouts and do not retry. Find the race or the shared state. Make it fail the same way every time, then fix the cause.
pitfall: Skipping the test to ship. That is firing the judge for being honest.
### 218 test double
id: test-double
url: https://vibe-coding-101.brighteng.org/c/test-double/
one_liner: A stand-in for a collaborator. A fake has behavior. A mock only remembers it was called.
also: mock, stub, fake
what: A test double replaces a dependency. A stub returns canned data. A fake is a simpler working version, such as an in-memory queue. A mock asserts the calls. If the double is the only thing the test exercises, you tested the double.
why_vibe: Models mock the database, the clock, and the function under test. The suite is green and the query is wrong. Use a fake with real behavior, or a database in a transaction you roll back.
move: Fake or call the real dependency when it has logic. Mock only an edge you do not own, such as a payment HTTP API. Do not mock the unit you are testing. Assert the result a caller would see.
tell_the_model: Test (behavior) with a fake (dependency), not a mock of the database. Do not mock the function under test. Assert the result a caller sees. A mock is only for an external API you cannot run.
pitfall: `toHaveBeenCalledWith` as the only assertion. An empty function that calls the mock and returns nothing still passes.
## model
Hallucination, system prompts, MCP, tokens, temperature, sandboxes. Words about the model itself.
### 30 hallucination
id: hallucination
url: https://vibe-coding-101.brighteng.org/c/hallucination/
one_liner: The model states a missing API, file, or fact as if it were real. Calm tone is not evidence.
what: A hallucination is a confident invention: a package that is not on npm, a flag that never existed, a function in the wrong file. It is not a lie with intent. It is next-token completion filling a hole.
why_vibe: Vibe coding dies here more than in syntax. You install the invented package, then blame the network. Run it. Read the file. Do not trust a stable paragraph.
move: Ban invented APIs in the prompt. After edits, grep for names you do not recognise. If a library is named, you install it and import it before the next turn.
tell_the_model: Do not invent APIs, flags, or files. Read the repo first. If you are not sure a symbol exists, say so and stop.
pitfall: Asking “are you sure?” The model will say yes. Verification is a command, not a question.
### 31 system prompt
id: system-prompt
url: https://vibe-coding-101.brighteng.org/c/system-prompt/
one_liner: The standing instructions above the chat: who the model is, what it may touch, how it should fail.
what: A system prompt is the persistent brief. User turns are episodes. The system prompt is the show bible: repo conventions, forbidden moves, the test command, the definition of done.
why_vibe: Without one, every turn renegotiates the job. With a sloppy one, the model role-plays “senior engineer” and still rewrites half the tree. Put mechanical limits in tools; put taste and procedure in the system prompt.
move: Keep a SYSTEM.md in the repo. Point the agent at it. Update it when a turn goes wrong, the same way you would fix a test.
tell_the_model: Follow SYSTEM.md. It outranks this turn. If this turn conflicts with it, stop and say so.
pitfall: A novel in the system prompt. Models lose the middle. Short, testable rules beat a manifesto.
### 32 few-shot
id: few-shot
url: https://vibe-coding-101.brighteng.org/c/few-shot/
one_liner: Show two or three worked examples in the prompt. The model copies the shape, not your adjectives.
what: Few-shot prompting is teaching by examples: input → output pairs in the prompt. Zero-shot is an instruction with no sample. One sloppy sample beats a paragraph of “write it cleanly.”
why_vibe: “Write tests like we do” is empty. Paste two existing tests. The model matches the fixture, the name, the assertion style. Examples are a spec the model can imitate.
move: When asking for a new function or test, paste two nearby examples from the repo. Say “match this shape.” Do not describe the house style in prose.
tell_the_model: Match these two examples. Same naming, same assertion style, same file layout. Then do (the new case).
pitfall: Examples that disagree with each other. The model averages them into a third dialect.
### 33 structured output
id: structured-output
url: https://vibe-coding-101.brighteng.org/c/structured-output/
one_liner: Force JSON or a schema. Prose from a model is a suggestion. A schema is a contract.
what: Structured output means the model must fill a schema: JSON, a typed object, a tool call. Invalid shape is a failed generation, not a vibe to parse by eye.
why_vibe: Agents that emit JSON you then exec are how tools work. If you let the model write a paragraph and you regex it, you will eat a fence. Schema first, like types-as-spec for the prompt.
move: For any machine-consumed answer, give a JSON schema. Reject extra keys. Parse, do not read.
tell_the_model: Reply with JSON only, matching this schema. No markdown, no preamble. Extra keys are an error.
pitfall: “JSON-ish” with a code fence and a note. That is prose. Fail the parse and retry.
### 34 MCP
id: mcp
url: https://vibe-coding-101.brighteng.org/c/mcp/
one_liner: A standard way to plug tools and data into the model: issue trackers, browsers, docs, databases.
also: Model Context Protocol
what: MCP is a protocol so an agent can call external tools through a shared shape instead of a one-off integration. Servers expose tools and resources; the client (your coding agent) calls them.
why_vibe: Without MCP you paste tickets and logs by hand. With it the model can read Linear, query a DB, or drive a browser — and also exfiltrate if you wired production. Treat MCP servers like shell access.
move: Enable only the servers this repo needs. Prefer read-only. Never point an agent at production credentials through MCP “to be helpful.”
tell_the_model: You may use these MCP tools: (list). Read-only. Do not call anything that writes to production. If a tool is missing, ask; do not invent a call.
pitfall: Installing every MCP server you saw on Twitter. Each one is another blast radius.
### 35 token budget
id: token-budget
url: https://vibe-coding-101.brighteng.org/c/token-budget/
one_liner: The window is finite. Every file you add crowds out the one that mattered.
what: Tokens are the unit of context: prompt, files, history, images. A token budget is the hard cap. Overflow is silent: old instructions fall out of the window, not onto the floor where you can see them.
why_vibe: A long vibe-coding thread feels informed and is actually amnesiac. The model “forgets” the spec because you buried it. Context engineering is budgeting, not hoarding.
move: New thread when the chat is fat. Re-pin the spec file. Do not dump the repo. Count files like they cost rent, because they do.
tell_the_model: Context is tight. Use only these files: (list). Do not reread the whole repo. The spec file is source of truth.
pitfall: “Add everything so it has full picture.” The full picture is how the spec falls out of the window.
### 36 temperature
id: temperature
url: https://vibe-coding-101.brighteng.org/c/temperature/
one_liner: A knob for randomness. Code wants it low. Brainstorming can stand it higher.
what: Temperature scales how wildly the model samples the next token. Low (0–0.3) is peaked and repeatable. High is diverse and sloppy. It is not “creativity.” It is entropy.
why_vibe: Generating production code at high temperature is how you get a different architecture every turn. Keep implementation cold. Raise it only for naming, copy, or a spike you will throw away.
move: Default the coding agent low. If two runs of the same prompt disagree on structure, temperature or the prompt is too loose — tighten the spec, do not roll the dice again.
tell_the_model: This is implementation, not brainstorming. Prefer the boring, local, already-used pattern. Do not surprise me with a new architecture.
pitfall: Turning temperature up because the model “feels stuck.” Stuck usually means the spec is vague. Fix the spec.
### 37 sandbox
id: sandbox
url: https://vibe-coding-101.brighteng.org/c/sandbox/
one_liner: A sealed place the agent can run commands. Network, filesystem, and secrets are opt-in.
what: A sandbox is an isolated runtime: the agent may compile and test without touching your real home directory, your prod DB, or the public net unless you opened a hole.
why_vibe: Tool-using agents are useful because they run things. They are dangerous for the same reason. A sandbox is a guardrail that does not rely on the model’s manners.
move: Run agents in a sandbox by default. Allow the test command. Deny `rm -rf`, prod URLs, and `.env`. If a task needs the network, open it for that turn only.
tell_the_model: You are in a sandbox. No network unless I say so. No files outside (paths). No secrets. Run (test command) as much as you want.
pitfall: Disabling the sandbox because “it could not pip install.” That error was the sandbox doing its job. Allow the one registry, do not open the world.
### 38 plan then act
id: plan-then-act
url: https://vibe-coding-101.brighteng.org/c/plan-then-act/
one_liner: The model writes the steps. You approve. Then it may touch files. Not the other way around.
what: Plan-then-act is a two-phase agent loop: a plan with no side effects, a human gate, then execution. ReAct mixes thought and tools in one stream; plan-then-act splits them on purpose.
why_vibe: Agents that “just start” will refactor while exploring. The plan is cheap. The diff is not. Make the plan the first artifact you accept.
move: Every non-trivial turn starts with “plan only.” Reject plans that touch more than the slice. Then a second turn: execute this plan, nothing else.
tell_the_model: Plan only. No file edits, no commands that write. Numbered steps, files you would touch, tests you would run. Wait for OK.
pitfall: Approving a plan that says “and clean up nearby.” Nearby is how a slice becomes a rewrite.
### 39 compaction
id: compaction
url: https://vibe-coding-101.brighteng.org/c/compaction/
one_liner: When the window fills, the agent summarises the chat. Details die in the summary. Pin what must live.
also: context rot
what: Compaction is automatic compression of old turns so the model can keep going. Context rot is the quality loss: names, constraints, and “do not do X” fall out of the summary.
why_vibe: A long vibe session feels continuous to you and is a new, lossy document to the model. The spec you stated at turn 3 may not exist at turn 40 unless it is in a file.
move: Put standing rules in files, not in chat. After compaction, restating the slice is cheaper than assuming memory. Start a new thread sooner than you think.
tell_the_model: If context was compacted, reread (spec file) before editing. Chat history is not the spec.
pitfall: Trusting “as we discussed earlier.” After compaction there is no earlier. There is a haiku of earlier.
## ship
Secrets, migrations, idempotency, flags, rollbacks. The layer the model fakes fluency in, and where it blows up.
### 40 env and secrets
id: env-secrets
url: https://vibe-coding-101.brighteng.org/c/env-secrets/
one_liner: Keys live in the environment, never in the repo, never in the prompt if you can help it.
what: Environment variables configure a process. Secrets are the subset that grant power: API keys, tokens, private URLs. `.env` is local. The committed `.env.example` has names, not values.
why_vibe: Models love to hardcode a key “just to get it running,” or paste `.env` into the chat to debug. That is how keys leak into git, logs, and training-adjacent clipboard history.
move: `.env` in gitignore. `.env.example` committed. Tell the agent never to print secrets. Rotate anything that appeared in a diff or a chat.
tell_the_model: Do not read, print, or commit secrets. Use env vars. If a key is missing, say which name is missing. Do not invent a placeholder key that looks real.
pitfall: A “dummy” key that is actually a live staging key. Dummy means obviously fake, or nothing.
### 42 migration
id: migration
url: https://vibe-coding-101.brighteng.org/c/migration/
one_liner: A versioned, reversible change to the schema. Not “edit the database until it works.”
what: A migration is a checked-in script that moves the schema from A to B, with a way back. Expand/contract: add the new column, ship readers, then drop the old one.
why_vibe: Models rewrite schema files in place and call it done. Production data is still on the old shape. You wanted a migration, not a new drawing of the table.
move: Never let the agent “just update the schema.” Demand a named migration, a rollback, and a note about existing rows.
tell_the_model: Add a migration, not a schema rewrite. Existing rows must stay valid. Include a down/rollback. Do not drop columns in the same step you add their replacement.
pitfall: Destructive migrations on the first try. Expand, ship, then contract. The model will skip the middle if you let it.
### 43 idempotency
id: idempotency
url: https://vibe-coding-101.brighteng.org/c/idempotency/
one_liner: Doing it twice has the same result as doing it once. Clicks, webhooks, and retries need this.
what: An idempotent operation can be repeated without extra effect. PUT and DELETE often are. POST that creates an order is not, unless you send an idempotency key.
why_vibe: The model writes a submit handler and a webhook and neither can survive a double fire. Users double-click. Stripe retries. You get two charges.
move: Any create-that-costs-money or create-that-emails gets a key. Store it. Return the original result on replay. Disable the button in flight as UX, not as the only lock.
tell_the_model: Make (action) idempotent. Same key, same result, no second (charge / email / row). Disable the button while in flight, but do not rely on the button.
pitfall: Trusting the UI disable. The retry is a webhook, a tab restore, or a fetch that ran twice. The lock lives on the server.
### 49 feature flag
id: feature-flag
url: https://vibe-coding-101.brighteng.org/c/feature-flag/
one_liner: Ship the code dark. Turn it on for some people. Turn it off without a rollback of the whole site.
what: A feature flag is a runtime switch in front of a behaviour. The code is in production; the experience is not, until the flag says so.
why_vibe: Vibe coding ships straight to everyone. A flag lets you merge incomplete work behind a door. It is not a substitute for tests; it is a substitute for praying.
move: New risky UI behind a flag, off by default, on for you. Kill the flag once it is the only path. Flags that live forever are another settings app.
tell_the_model: Put (feature) behind a feature flag, off by default. Do not gate it only in the UI; the API must refuse too. Do not invent a whole flag admin in this slice.
pitfall: A flag checked only in React. The API still does the new thing. That is not a flag. That is a costume.
### 50 rollback
id: rollback
url: https://vibe-coding-101.brighteng.org/c/rollback/
one_liner: A rehearsed way back. If you cannot undo the deploy, you did not finish the change.
what: Rollback returns production to a known good version. Revert the release, not “hotfix forward” while the room is on fire — unless forward is the only option (a bad migration).
why_vibe: Agents ship. They do not plan the undo. Before you merge a vibe-coded slice that touches data or money, ask how you turn it off: flag, revert, or migration down.
move: Prefer revert of the release artifact. For schema, expand/contract so rollback of code is still valid against the DB. Practice it once on preview.
tell_the_model: This change must be rollback-safe. No irreversible data rewrite. If a migration is needed, expand first so old code still runs.
pitfall: “We’ll just deploy a fix.” That is a hope. A rollback is a button you already tested.
## engineering
Dependency injection, leaky abstractions, semver, observability. Software-engineering names the model will not pick unless you do.
### 56 dependency injection
id: dependency-injection
url: https://vibe-coding-101.brighteng.org/c/dependency-injection/
one_liner: Pass collaborators in. Do not let a function new up the world.
also: DI
what: Dependency injection means a unit receives the things it uses — a clock, a database, a mailer — instead of constructing them. Tests can pass fakes. Production passes the real ones.
why_vibe: Models hide `new Stripe()` and `fetch` inside business code. You cannot test the rule without the network. Name DI or every “unit test” becomes an integration accident.
move: Constructor or function argument for anything that talks to time, disk, or the network. One composition root wires the real ones. Tests pass fakes.
tell_the_model: Inject the (clock / db / mailer). Do not construct it inside the function. Tests must pass a fake. Wire the real one in one place.
pitfall: A DI container for three classes. You wanted a parameter, not a framework.
### 57 separation of concerns
id: separation-of-concerns
url: https://vibe-coding-101.brighteng.org/c/separation-of-concerns/
one_liner: UI does not own the rule. The rule does not own SQL. Each file has one reason to change.
what: Separation of concerns splits a system by what changes together: presentation, domain rule, persistence, transport. A change to the button should not rewrite the invariant.
why_vibe: A model’s default is one file that fetches, decides, and renders. The next prompt then edits all three. Name the boundary or every slice becomes a ball of mud.
move: Before generating, name the layers for this slice: handler, rule, store. Tell the model which file owns which. Reject a function that does all three.
tell_the_model: Separate concerns. The handler parses input. The domain function decides. The store writes. Do not put SQL or JSX in the rule.
pitfall: Six folders and one function that still does everything, imported across them. Folders are not boundaries.
### 58 leaky abstraction
id: leaky-abstraction
url: https://vibe-coding-101.brighteng.org/c/leaky-abstraction/
one_liner: The wrapper still forces you to know what is underneath. Then it is not a wrapper.
what: A leaky abstraction hides a system but lets its accidents through: SQL errors in the UI, HTTP status codes in the domain, a “repository” that returns the ORM row.
why_vibe: Models love a `utils` layer that re-exports the library with the same arguments. You paid for an abstraction and still have to know Stripe’s error shape. Name the leak.
move: At each boundary, map to your nouns. The UI sees “payment failed,” not `card_declined`. If the caller must know the inner type, the abstraction failed.
tell_the_model: Do not leak (library) types past this boundary. Map errors and rows to our types. Callers must not import the SDK.
pitfall: Wrapping every call in `try/catch` that rethrows the same error. That is a costume.
### 59 technical debt
id: technical-debt
url: https://vibe-coding-101.brighteng.org/c/technical-debt/
one_liner: A shortcut with interest. Write down the principal, the interest, and the due date.
what: Technical debt is a deliberate or accidental shortcut that makes the next change more expensive. Debt without a note is just rot. Debt with a ticket is a loan.
why_vibe: Models ship the shortcut and the apology in the same turn, then forget. Next session they build on the shortcut. Name the debt or it becomes the architecture.
move: If you accept a shortcut, file it: what is wrong, what it costs, when to pay. Do not let the model “clean it up later” without a file in the repo.
tell_the_model: This shortcut is technical debt. Do the small version. Add a comment and a TODO naming the cost. Do not also build the “proper” version in the same turn.
pitfall: Calling every dislike debt. Debt is a future cost, not a taste complaint.
### 60 semver
id: semver
url: https://vibe-coding-101.brighteng.org/c/semver/
one_liner: MAJOR breaks callers. MINOR adds. PATCH fixes. The number is a promise, not a vibe.
also: semantic versioning
what: Semantic versioning is MAJOR.MINOR.PATCH. Breaking an existing caller is major. A compatible addition is minor. A bugfix that keeps the contract is patch.
why_vibe: Models bump versions at random, or publish a breaking rename as 1.0.1. Downstream vibe-coded apps then explode on install. Name semver and name what changed for callers.
move: Before a release, list caller-visible changes. If any name, type, or required field changed, it is major. Do not “just bump patch.”
tell_the_model: Follow semver. This change (breaks / adds / fixes) callers, so bump (major / minor / patch). Do not renumber for taste.
pitfall: 0.x forever so you never have to mean it. Pre-1.0 still needs a note when you break people.
### 61 breaking change
id: breaking-change
url: https://vibe-coding-101.brighteng.org/c/breaking-change/
one_liner: Existing callers fail without edits. Renames, removed fields, stricter validation.
what: A breaking change makes previously valid use invalid: deleted endpoints, renamed props, a field that used to be optional and is now required, a new error for an old input.
why_vibe: Models “clean up” an API and call it a refactor. Callers in other repos, mobile apps, and webhooks do not recompile with you. Name the break or you will ship it as a drive-by.
move: Grep callers, including mobile and external docs. If you must break, version the route or keep the old field one release. Write the migration for the caller, not only the server.
tell_the_model: Do not break callers. Keep (old field / route) working. If a break is required, add it beside the old one and list every caller that must change.
pitfall: “Nobody uses that.” Somebody’s Friday script does. Search before you delete.
### 62 observability
id: observability
url: https://vibe-coding-101.brighteng.org/c/observability/
one_liner: Logs say what happened. Metrics say how often. Traces say which hop was slow.
also: logs, metrics, traces
what: Observability is being able to ask a new question of a running system without shipping a new log line first. The three tools are structured logs, metrics, and traces. `console.log("here")` is none of them.
why_vibe: Models sprinkle `console.log` and call the app debuggable. In production you cannot attach a debugger to the vibe. Name the three signals or the next outage is a guessing game.
move: One structured log per request with an id. One metric for the user-visible failure. A trace across the handler and the database. No secrets in any of them.
tell_the_model: Add observability, not print debugging. Structured log with a request id, one metric for failures, and a trace across handler and database. Do not log secrets or full payloads.
pitfall: Logging every object. That is a bill and a leak, not observability.
### 63 architecture decision record
id: adr
url: https://vibe-coding-101.brighteng.org/c/adr/
one_liner: A short note: the decision, the context, the options you rejected. So next month’s chat does not relitigate it.
also: ADR
what: An ADR is a dated page in the repo. Context, decision, consequences. It is not a design novel. It is why the code looks like this.
why_vibe: A new thread will “improve” the architecture you already chose, because the reason lived in chat. Pin it in `docs/adr`. Tell the model to read it before proposing a second approach.
move: When a choice has a real alternative — library, storage, auth — write a one-page ADR before generating the second option. Status: accepted. Link it from the prompt.
tell_the_model: Read docs/adr before proposing a new architecture. If this decision is new, write an ADR: context, decision, rejected options. Do not implement two approaches.
pitfall: An ADR after the rewrite, describing what you wish you had decided. Write it when the choice is made, not as fan fiction.
### 64 invariant
id: invariant
url: https://vibe-coding-101.brighteng.org/c/invariant/
one_liner: A fact that must stay true. Balance not negative. A child row never outlives its parent.
what: An invariant is a condition the system must hold before and after every change. Types can encode some. Databases encode others. Tests encode the rest. If it can be false, it is not an invariant yet — it is a hope.
why_vibe: Models implement the happy update and leave the illegal state representable. Name the invariant and you get a constraint, not a comment.
move: For each object, write the sentences that must always be true. Put each in the strongest place: type, check constraint, or test. Reject “we’ll validate in the UI.”
tell_the_model: Enforce this invariant: (fact). Prefer the database or the type. A UI check is not enough. Add a test that tries to break it.
pitfall: An invariant written only in a docstring. The model will not read it next week. The database will.
### 65 composition over inheritance
id: composition
url: https://vibe-coding-101.brighteng.org/c/composition/
one_liner: Has-a, not is-a. Assemble small pieces. Do not grow a base class the model keeps subclassing.
what: Composition builds behaviour by holding other objects. Inheritance builds it by subclassing. Deep hierarchies share code and also share surprises. Composition shares only what you pass in.
why_vibe: Models reach for `BaseService` and `AbstractRepository` on turn one. By turn five you have a diamond and no one knows which `save()` runs. Say composition or you inherit a framework you did not order.
move: Ban new base classes unless an existing one is already the pattern. Prefer a function or a small collaborator. If two classes share ten lines, extract a function, not a parent.
tell_the_model: Use composition, not inheritance. Do not add a base class. Share behaviour with a function or an injected collaborator.
pitfall: A “mixin” pile that is inheritance with extra steps. If you cannot construct the piece alone, it is not composition.
## backend
REST, auth, N+1, money, time zones, transactions, queues. The server words a model writes fluently and wrongly.
### 45 N+1 query
id: n-plus-one
url: https://vibe-coding-101.brighteng.org/c/n-plus-one/
one_liner: One query to list N rows, then one query per row. Fine at 3. Dead at 300.
what: N+1 is a loop that queries per item after fetching the list. ORMs do this when you lazy-load relations in a template.
why_vibe: The model writes a list page that looks right with seed data. Then a real workspace has 400 projects and the page takes twelve seconds. Name N+1 or you will ship it.
move: For any list, one query with a join or an `include`. Log SQL in dev. If you see a query per row, that is the bug, not “the DB is slow.”
tell_the_model: No N+1. The (list) page must not query per item. Load relations in one query. If you use an ORM, include/join them up front.
pitfall: Caching the N+1. You cached the wound. Fix the query shape.
### 48 webhook
id: webhook
url: https://vibe-coding-101.brighteng.org/c/webhook/
one_liner: Their server hits yours when something happens. Verify the signature. Do not trust the browser return.
what: A webhook is an HTTP callback from another system: payment succeeded, repo pushed, email opened. You expose an endpoint; they POST. Retries are normal.
why_vibe: Models mark an order paid when Stripe redirects the browser. The user closed the tab. The webhook is the source of truth. Also: unsigned webhooks are a write API for strangers.
move: Verify signatures. Make handlers idempotent. Process out of band if work is slow. Never take payment state from a query string.
tell_the_model: Mark (object) paid from the webhook, not from the browser return. Verify the signature. The handler must be idempotent; providers retry.
pitfall: Returning 200 before you persisted. Their retry storms, or they never retry and you lost the event.
### 66 REST
id: rest
url: https://vibe-coding-101.brighteng.org/c/rest/
one_liner: Resources and verbs the HTTP spec already has. GET reads. PUT replaces. POST creates. DELETE removes.
what: REST models the API as resources with uniform verbs, status codes, and links. It is not “JSON over POST for everything.” GET must be safe. PUT and DELETE should be idempotent.
why_vibe: Models invent `/doThing` POST endpoints and return 200 for failures. Caches, retries, and clients then lie. Name REST and you get nouns, verbs, and status codes.
move: Name the resource. Map the action to a verb. Pick the status: 201 created, 204 no body, 404 missing, 409 conflict, 422 invalid. Do not return 200 with `{ ok: false }`.
tell_the_model: REST for (resource). GET is safe. PUT replaces. POST creates. Use real status codes, not 200 with an error field. Do not invent RPC-style /doThing routes.
pitfall: A REST lecture and a single POST /api. If every action is POST, say RPC and own it. Do not cosplay.
### 67 authn vs authz
id: authn-authz
url: https://vibe-coding-101.brighteng.org/c/authn-authz/
one_liner: Authn is who you are. Authz is what you may do. Logging in is not permission.
also: authentication vs authorization
what: Authentication proves identity: password, passkey, session. Authorization decides an action on a resource: this user may edit this row. They fail differently and belong in different checks.
why_vibe: Models add login and stop. Every route then trusts “any logged-in user.” That is how one customer reads another’s invoices. Say both words.
move: Every mutating route: identify the caller, then authorize the specific object. Test both a stranger and a logged-in stranger. 401 means not identified. 403 means identified and refused.
tell_the_model: Separate authn and authz. 401 if there is no session. 403 if the session may not touch this (object). Do not treat “logged in” as permission.
pitfall: Hiding the button and leaving the route open. The UI is not the server.
### 68 pagination
id: pagination
url: https://vibe-coding-101.brighteng.org/c/pagination/
one_liner: Do not return the whole table. Offset pages lie under inserts. Cursors stay stable.
also: cursor vs offset
what: Pagination returns a window of a list plus a way to ask for the next window. Offset is `page * size` and skips or duplicates when rows change. A cursor points at the last seen row and asks for what comes after.
why_vibe: Models `SELECT *` or `LIMIT 1000`. The first demo looks fine. The real tenant has 40,000 rows. Name pagination before the list endpoint exists.
move: Default a cursor on any list that can grow. Stable sort (created_at, id). Return `next_cursor`. Cap page size. Do not offset a live feed.
tell_the_model: Paginate (list) with a cursor, not OFFSET. Stable order by (field, id). Return next_cursor. Cap page size. Do not load every row.
pitfall: Infinite scroll on an admin table you must jump around. That wants pages. A feed wants a cursor. Say which.
### 69 rate limit
id: rate-limit
url: https://vibe-coding-101.brighteng.org/c/rate-limit/
one_liner: A cap on how often a caller may hit you. 429, not a meltdown.
what: A rate limit counts requests per key — IP, user, token — over a window. Over the cap, refuse with 429 and a Retry-After. It is not a substitute for auth, and auth is not a substitute for it.
why_vibe: Models ship a public POST that sends email or calls a paid API. One loop burns the budget. Name the limit, the key, and the status.
move: Limit login, password reset, and anything that spends money or sends a message. Key by user when you have one, else IP. Return 429. Do not silently drop.
tell_the_model: Rate-limit (route): N per window per (user or IP). Over the cap, respond 429 with Retry-After. Do not only hide the button.
pitfall: A global limit so one busy customer locks the API for everyone. Partition the key.
### 70 transaction
id: transaction
url: https://vibe-coding-101.brighteng.org/c/transaction/
one_liner: Several writes that commit together or not at all. A half-saved order is a bug.
also: ACID
what: A transaction groups reads and writes so they are atomic, consistent, isolated, and durable. Either every statement lands or none do. Isolation is how two requests avoid seeing each other’s half-work.
why_vibe: Models write “create order, then charge, then email” as three awaits. The process dies after the charge. You have money and no order. Say transaction or saga, and mean one of them.
move: One database: one transaction around the invariants. More than one system: do not pretend a transaction — use an outbox or a saga. Never hold a transaction open across an HTTP call.
tell_the_model: Wrap (writes) in one database transaction. Commit only if all succeed. Do not call the network inside the transaction. If two systems are involved, say so and use an outbox, not a fake transaction.
pitfall: `BEGIN` and a forgotten `COMMIT` on the pool, so later requests hang. Keep the scope tiny.
### 71 database index
id: db-index
url: https://vibe-coding-101.brighteng.org/c/db-index/
one_liner: A lookup structure so the query does not read the whole table. Indexes match the WHERE and the ORDER BY.
what: An index lets the database find rows without a scan. It costs writes and disk. A missing index is a sequential scan. A useless index is write amplification. Unique indexes are also constraints.
why_vibe: Models add a filter in the API and never the index. The page is fast on three seed rows. Name the query and the index in the same turn.
move: For every new list or lookup, write the query, then the index that matches its filter and sort. Explain with `EXPLAIN` if you can. Do not index every column “to be safe.”
tell_the_model: Add an index for the query that filters by (columns) and sorts by (columns). Do not index columns this query does not use. Keep uniqueness as a unique index, not an application check.
pitfall: An index on a low-cardinality boolean alone. It will not save you. Lead with the selective column.
### 72 cache invalidation
id: cache-invalidation
url: https://vibe-coding-101.brighteng.org/c/cache-invalidation/
one_liner: A cache is a lie with a deadline. Say who deletes it, and when.
what: Invalidation removes or refreshes a cached value when the source changes. TTL alone means you serve stale data until the clock saves you. Explicit purge means the write path knows the key.
why_vibe: Models add Redis or HTTP cache headers and never a purge. You change a title and the public page stays old. Name the key and the event that kills it.
move: Every cache key has a writer who deletes or updates it. Prefer “purge on write” for anything a human just edited. TTL is for tolerance, not for correctness.
tell_the_model: Cache (thing) under a key that includes (id). On write, delete that key. Do not rely on a long TTL for a page a user just edited.
pitfall: Caching a per-user response under a global key. One user’s data becomes everyone’s. The key must include the viewer.
### 73 job queue
id: job-queue
url: https://vibe-coding-101.brighteng.org/c/job-queue/
one_liner: Work that must not live inside the request: mail, thumbnails, webhooks you send. Durable, retried, idempotent.
what: A job queue stores work for a worker to run later. The HTTP request enqueues and returns. The worker retries with backoff. If the process dies, the job remains.
why_vibe: Models `await sendEmail()` inside the POST. The user stares at a spinner, and a timeout loses the send. Name the queue or the request owns work it cannot finish.
move: If it can be slow, fail, or be retried, it is a job. The request writes the row and enqueues. The worker is idempotent. Failed jobs are visible, not swallowed.
tell_the_model: Do not do (slow work) in the request. Enqueue a job. The handler returns after the enqueue. The worker must be idempotent and retry with backoff. Record failures.
pitfall: `setTimeout` or an in-memory array. That dies with the process. A queue is durable.
### 74 circuit breaker
id: circuit-breaker
url: https://vibe-coding-101.brighteng.org/c/circuit-breaker/
one_liner: Stop calling a dependency that is already failing. Fail fast, then try again later.
what: A circuit breaker counts failures to a dependency. Past a threshold it opens: calls fail immediately without waiting. After a cool-down it half-opens and lets one trial through. Closed means healthy.
why_vibe: Models retry forever with no cap. One dead upstream then ties every request to a 30s timeout. Name the breaker, the threshold, and the fallback.
move: Timeouts first. Then a limit on retries. Then a breaker around the flaky client. When open, return a clear error or a stale cache — do not hang.
tell_the_model: Put a circuit breaker around (client). Timeout each call. After (N) failures, fail fast. Half-open later. Do not retry forever. Do not wait on a dead upstream.
pitfall: A breaker that opens and never closes, so you “fixed” the outage by staying down. Half-open is the point.
### 75 graceful shutdown
id: graceful-shutdown
url: https://vibe-coding-101.brighteng.org/c/graceful-shutdown/
one_liner: On SIGTERM, stop taking work, finish what you started, then exit. Do not drop the request mid-write.
what: Graceful shutdown is the process lifecycle at the end: catch the signal, stop the listener, drain in-flight requests and jobs, close pools, then exit 0. Kill -9 is not a strategy.
why_vibe: Deploys send SIGTERM. Models ignore it. A rolling restart then cuts payments in half. Name drain and a deadline.
move: Handle SIGTERM and SIGINT. `server.close()`. Stop fetching new jobs. Wait up to N seconds. Then disconnect the pool. Log that you drained.
tell_the_model: Handle SIGTERM. Stop accepting connections, finish in-flight requests, stop the worker after the current job, close the pool, then exit. Bound the wait.
pitfall: Awaiting the drain with no timeout. A stuck request then blocks the deploy forever.
### 261 event loop
id: event-loop
url: https://vibe-coding-101.brighteng.org/c/event-loop/
one_liner: One thread runs your JavaScript. A long synchronous call blocks every request on it.
also: microtask, macrotask, Node event loop
what: The event loop runs the call stack, then microtasks such as promise reactions, then one macrotask such as a timer or I/O callback. Node shares that loop across requests on the thread. fs.readFileSync, a huge JSON.parse, or synchronous crypto stops every other request. Promises are not parallelism.
why_vibe: The draft hashes a password with a sync call, or reads a file with readFileSync, inside the handler. Under one user it is fine. Under ten, the process stops answering health checks.
move: Keep the handler async and short. Move CPU work to a worker or a queue. Do not use the sync filesystem or sync crypto on the request path.
tell_the_model: Handle (request) on the event loop without blocking. Use async I/O. Do not call readFileSync, bcrypt sync, or a long JSON parse in the handler. Move CPU work to a worker.
pitfall: A microtask loop that never returns to the macrotask queue. Timers and I/O starve while promises schedule more promises.
### 262 GIL
id: gil
url: https://vibe-coding-101.brighteng.org/c/gil/
one_liner: CPython runs one thread of bytecode at a time. Threads do not speed up a pure Python loop.
also: global interpreter lock, CPython
what: The global interpreter lock lets one thread execute Python bytecode. Threads still help when the work releases the lock, which much I/O and many C extensions do. CPU-bound Python needs processes or native code. asyncio is concurrency on one thread. It does not use extra cores.
why_vibe: The draft starts a ThreadPoolExecutor to speed up a pure Python transform. The cores sit idle and the lock bounces. Or it uses asyncio.gather for CPU work and blocks the loop.
move: Use threads for I/O that releases the GIL. Use processes or a native extension for CPU. Do not put CPU work on the asyncio loop. Say which one you are doing.
tell_the_model: (Work) is CPU-bound Python. Do not use threads and expect a speedup under the GIL. Use processes or native code. Keep the asyncio loop free of CPU work.
pitfall: A process pool that shares a live socket or a database connection. Those objects do not survive the boundary. Pass data, not connections.
### 263 ownership
id: ownership
url: https://vibe-coding-101.brighteng.org/c/ownership/
one_liner: One owner. Borrows are temporary. Cloning to silence the checker keeps the cost forever.
also: borrow checker, Rust lifetimes
what: Ownership means each value has one owner, and it is dropped when the owner ends. A borrow is a shared or a mutable reference, not both, and it must not outlive the value. The borrow checker rejects use-after-free. A lifetime is part of the type. unsafe is you promising the checker’s rules still hold.
why_vibe: The draft clones every argument, or it wraps everything in Rc and RefCell, or it adds an unsafe block so the error goes away. The program compiles and the aliasing bug is back.
move: Own data in the struct that keeps it. Borrow for the length of the call. Clone only when a second owner is the point. Do not use unsafe to pass the compiler.
tell_the_model: Fix the ownership error in (function) without cloning by default and without unsafe. Borrow (value) for the call. Store it only if this struct should own it. Do not wrap it in Rc to silence the checker.
pitfall: A lifetime annotation copied from the error until it compiles. The annotation is now a lie about how long the data lives.
### 264 goroutine
id: goroutine
url: https://vibe-coding-101.brighteng.org/c/goroutine/
one_liner: A goroutine is cheap to start and easy to leak. Cancel it, or it runs until the process dies.
also: Go channel, goroutine leak
what: A goroutine is a lightweight thread scheduled by the Go runtime. A leak is one blocked forever on a channel send, a channel receive, or a call with no timeout. The context is how you cancel a tree of work. GOMAXPROCS is the number of OS threads, not the number of goroutines you may start.
why_vibe: The handler does go doWork() and returns. The work outlives the request, uses a closed context, and piles up. Nothing is waiting for the error.
move: Start a goroutine with a context that dies when the request or the owner dies. Bound how many run. Make the channel send select on cancellation. Wait for shutdown.
tell_the_model: Run (work) in a goroutine tied to (context). Stop when the context is cancelled. Bound the number in flight. Do not start one per request and ignore it. Surface the error.
pitfall: A channel with no buffer and no receiver yet. The send blocks forever and the goroutine never reaches your cancel check.
### 265 minor units
id: minor-units
url: https://vibe-coding-101.brighteng.org/c/minor-units/
one_liner: Money is an integer of the smallest unit, plus a currency. It is not a float.
also: cents, money integer, decimal scale
what: A minor unit is the smallest currency division: cents for USD, yen has zero decimal places. Store an integer count of those units and an ISO currency code. Binary floats cannot represent 0.10. A decimal type is acceptable when the scale is fixed and you do not mix it with float math.
why_vibe: The draft uses float64 or a JavaScript number for a price. 0.1 plus 0.2 is not 0.3. A sum of line items is a cent off, and the payout job “fixes” it by rounding at the end.
move: Integer minor units in storage and APIs. Format for humans at the edge. Do not add values of different currencies. Do not round a float back into cents.
tell_the_model: Store (price) as integer minor units and an ISO currency code. Do not use float or a JavaScript number. Do not add two currencies. Format the decimal only in the response.
pitfall: Multiplying a minor-unit integer by a float tax rate. The tax is a float again. Compute the rate in integer arithmetic or a fixed-scale decimal.
### 266 UTC vs local time
id: utc-local
url: https://vibe-coding-101.brighteng.org/c/utc-local/
one_liner: Store an instant in UTC. Store a birthday as a date. Do not confuse the two.
also: timezone, DST, civil date
what: An instant is a point on the timeline. Store it in UTC, or with an explicit offset. A civil date, such as a birthday or a store’s opening day, has no zone and must not be converted through one. Local wall time needs a zone id, because “2:30” does not exist on the DST spring-forward morning and happens twice in the fall.
why_vibe: The draft saves a timestamp without a zone, or it stores the user’s local time as if it were UTC. Reports shift by hours. A birthday becomes the day before for anyone west of the server.
move: Instants in UTC. Civil dates as dates. Convert to a zone at the edge, with the zone id stored if you need that wall time again. Test a DST gap.
tell_the_model: Store (event time) as a UTC instant. Store (birthday) as a date with no zone. Convert to the user zone only when rendering. Do not store local wall time without a zone id. Handle the DST gap.
pitfall: Truncating a UTC instant to a date on the server. The user’s local date is a different day.
### 267 optimistic concurrency
id: optimistic-lock
url: https://vibe-coding-101.brighteng.org/c/optimistic-lock/
one_liner: Write only if the version you read is still current. Zero rows updated means someone else wrote.
also: version column, ETag, compare-and-swap
what: Optimistic concurrency lets readers proceed, and the write checks a version, an updated_at, or an ETag. The update is `where id = ? and version = ?`. A count of zero is a conflict, returned as 409, not a success. The client refetches and retries. This is not optimistic UI, which only changes the screen before the response.
why_vibe: Two edits load the same row. Both save. The second form replaces the first person’s fields with blanks they never saw. The draft had no version in the WHERE clause.
move: Send the version you read. Update with it in the predicate. Increment it on success. On conflict, return 409 and the current row. Do not last-write-wins a form.
tell_the_model: Update (row) only if version is the one the client read. Increment the version. If no row changes, return 409 with the current resource. Do not overwrite with a stale form.
pitfall: Checking the version in application code, then updating without it in the SQL. Two requests both pass the check.
### 268 partial update
id: partial-update
url: https://vibe-coding-101.brighteng.org/c/partial-update/
one_liner: PATCH changes the fields it names. A missing field is left alone, not cleared.
also: PATCH, JSON Merge Patch
what: PUT replaces the resource. PATCH applies a change. In a merge patch, an absent field stays as it was, and null is how you clear, if your schema says null is allowed. JSON Patch is a list of operations. Treating a PATCH body as a full object fills the missing fields with defaults and wipes them.
why_vibe: The client sends `{ "name": "Ada" }` to change a name. The handler binds it to the entity, and the other columns become zero or null. The draft called that PATCH.
move: Apply only fields present in the document. Distinguish absent from null. Validate the result. Use the version check so two partial edits do not clobber each other.
tell_the_model: PATCH (resource): change only fields present in the body. Do not set absent fields to null or to a default. Null may clear a field only if the schema allows it. Reject unknown fields.
pitfall: A PATCH that triggers the same “create with defaults” binder as POST. Omitted required fields fail, or omitted optional fields reset.
### 269 transaction isolation
id: isolation
url: https://vibe-coding-101.brighteng.org/c/isolation/
one_liner: The default isolation level is not “as if one at a time.” Serializable can fail, and you retry.
also: read committed, repeatable read, serializable
what: Isolation is what a transaction is allowed to observe of other transactions. Read committed lets the same query see different rows a moment later. Repeatable read does not mean the same thing on Postgres and MySQL. Serializable is the level that means one-at-a-time, and the database may abort you with a serialization error you must retry. Phantoms and lost updates live in the gap.
why_vibe: The draft begins a transaction, reads a balance, calls another service, then writes. It assumed the read was stable. Under the default level it was not, and the HTTP call held a connection the whole time.
move: Name the anomaly you cannot allow. Pick the level that forbids it. Retry serialization failures. Keep the transaction short. Do not call the network while it is open.
tell_the_model: Run (operation) in a transaction at (isolation level). Retry if the database reports a serialization failure. Do not hold the transaction open across an HTTP call. Do not assume the default level is serializable.
pitfall: Catching the serialization error and returning 500. The correct response is to retry the transaction a bounded number of times.
### 270 DTO
id: dto
url: https://vibe-coding-101.brighteng.org/c/dto/
one_liner: The API type is not the table type. Do not bind a request onto the entity.
also: response model, mass assignment
what: A DTO, or response model, is the type at the boundary. It hides columns, renames fields, and stays stable when the schema changes. Binding a request body onto the entity is mass assignment: the client sets role, price, or owner_id because those columns exist. Returning the entity leaks fields you added later.
why_vibe: The handler returns the ORM object and “deletes password” on one path. The next endpoint forgets. Or the update endpoint accepts the entity, and a client sends is_admin: true.
move: One input type and one output type per endpoint. List the fields. Ignore the rest. Map to the entity in code you can see. Do not spread the request onto the model.
tell_the_model: Define an input DTO and an output DTO for (endpoint). Do not bind the request onto the entity. Do not return the ORM model. Ignore fields that are not on the DTO, including role and owner.
pitfall: A DTO that inherits the entity “to stay in sync.” You reopened the hole and gave it a new name.
### 271 prepared statement
id: prepared-statement
url: https://vibe-coding-101.brighteng.org/c/prepared-statement/
one_liner: The SQL string is constant. Values are parameters. Escaping quotes is not that.
also: parameterized query, bind parameter
what: A prepared statement sends the query shape and the values separately. The database never parses a value as SQL. String concatenation, format strings, and an ORM method that interpolates a fragment are injection. Escaping quotes misses encodings, identifiers, and the next person who adds one more clause.
why_vibe: The draft glues the name into the SQL string, or calls a raw query with a formatted string. A test name with a quote breaks it. A hostile name changes the query.
move: Parameters for values. If an identifier must vary, choose it from a fixed allowlist in code. Do not escape. Do not build SQL with a template string.
tell_the_model: Query (table) with a prepared statement. Bind (values) as parameters. Keep the SQL string constant. Do not concatenate or escape input into the SQL. If a column name varies, pick it from an allowlist.
pitfall: Parameters for values, and a template string for the ORDER BY column. The column is still injection.
### 272 WebSocket
id: websocket
url: https://vibe-coding-101.brighteng.org/c/websocket/
one_liner: A WebSocket is a long-lived connection. Auth happens on the upgrade, and a slow client must not fill memory.
also: socket, upgrade handshake
what: A WebSocket starts as an HTTP upgrade and then carries frames both ways. The handshake is where you check the cookie or a one-time ticket. A token you meant to check on every message will be forgotten on one path. Heartbeats tell you the peer is gone. Reconnect needs backoff. If the client reads slowly, you apply backpressure instead of buffering forever.
why_vibe: The draft uses a WebSocket for one request and response, or it trusts the first message as login and never checks the handshake. A disconnected client leaves a goroutine writing into a buffer.
move: Authenticate the upgrade. Heartbeat. Bound the send buffer. Reconnect with backoff and a fresh ticket. Use plain HTTP when one response would do.
tell_the_model: Open a WebSocket for (stream). Authenticate the upgrade. Heartbeat. Apply backpressure when the client is slow. Reconnect with backoff. Do not use a socket for a single request and response.
pitfall: A load balancer that drops idle connections at 60 seconds while your heartbeat is at 90. The socket dies and only the client notices.
### 273 error as value
id: error-as-value
url: https://vibe-coding-101.brighteng.org/c/error-as-value/
one_liner: Return the error or throw it. Do not do both, and do not swallow either.
also: Result, Go error
what: Some languages return a Result or an error value. Others throw. A boundary picks one. An empty catch, a logged-and-ignored error, and a null that means both “missing” and “broken” hide different failures. A panic or an exception is for a programmer bug. A user-facing failure is a value you handle.
why_vibe: The draft catches Exception, logs it, and returns null. The caller treats null as not-found and tells the user the row is gone. The database was down.
move: Return a typed error for expected failures. Crash, loudly, on a broken invariant. Do not catch the base exception. Do not map every failure to null or to 200.
tell_the_model: Return errors from (function) as values the caller must handle. Do not catch the base exception. Do not return null for both not-found and a broken dependency. Not-found is (type). A down dependency is (type).
pitfall: Wrapping every error in a new one until the original code and the stack are gone. The log says “failed” and nothing else.
### 274 request scope
id: request-scope
url: https://vibe-coding-101.brighteng.org/c/request-scope/
one_liner: One database session for the request. Close it before you return. Do not share it.
also: unit of work, scoped session
what: A request scope is the unit of work for one HTTP request or one job: open a session, do the work, commit or roll back, close in a finally. A session is not safe across threads or across requests. A global session races. A session cached on the application object leaks the first request’s transaction into the second.
why_vibe: The draft creates one session at startup and uses it in every handler. Two requests interleave. Or it opens a session per row. Or a background task uses the request session after the response has closed it.
move: Open at the boundary, close in finally. One commit. A job gets its own scope. Do not store the session in a global or pass it to fire-and-forget work.
tell_the_model: Open one database session per request for (handler). Commit or roll back once. Close it in a finally. Do not share it across requests or threads. A background job opens its own session.
pitfall: A middleware that opens the session and a handler that also opens one. You now have two transactions and you commit the wrong one.
### 275 option type
id: option-type
url: https://vibe-coding-101.brighteng.org/c/option-type/
one_liner: Missing, null, and empty are three states. Do not collapse them at the door.
also: Option, Maybe, null vs undefined
what: An option type is a value that is present or absent, and the caller has to handle both. JSON adds a third: a field left out, a field set to null, and a field set to empty string are different. undefined and null are different in JavaScript. Coalescing them with a default at the parser throws away what the client sent.
why_vibe: The draft uses `value ?? default` on a PATCH body. A client that sent null to clear a field, and a client that omitted the field, become the same. The clear never happens, or every omit clears.
move: Represent absence in the type. At a JSON boundary, decide absent versus null on purpose and test both. Do not optional-chain past a bug so the screen renders blank.
tell_the_model: Represent (field) as an option. On input, treat a missing field and null as different cases. Do not coalesce them with a default. Handle the absent case at the call site.
pitfall: An optional field on the domain type because the parser was unsure. The uncertainty leaked into every caller.
### 276 DataLoader
id: dataloader
url: https://vibe-coding-101.brighteng.org/c/dataloader/
one_liner: Collect ids during one request and load them in one query. The cache dies with the request.
also: request-scoped batching
what: A DataLoader batches keys requested in the same tick and caches the results. It turns a loop of findById into one where-in. The cache is per request. A cache that lives on the process serves one user another user’s row. A loader does not replace a join you could have written when you already knew the query.
why_vibe: A GraphQL resolver calls the database per field. The draft then makes a process-wide cache to “fix N+1.” The cache has no tenant key and no expiry. Or it creates a new loader inside the resolver, so nothing is ever batched.
move: One loader per request, per entity. Batch by id. Include the tenant in the key if the id is not globally unique. Do not store the loader on the server object.
tell_the_model: Batch (entity) loads with a DataLoader created per request. Do not query inside the loop. Do not cache across requests. Include the tenant in the key. Do not construct the loader inside the resolver.
pitfall: Awaiting inside the loop before the loader can see the rest of the keys. You serialized the batches back into N queries.
### 277 middleware order
id: middleware-order
url: https://vibe-coding-101.brighteng.org/c/middleware-order/
one_liner: The first middleware sees the request first. Auth at the bottom of the stack is auth that never runs in time.
also: middleware pipeline
what: Middleware is a stack. The first one registered usually sees the request before the handler and the response on the way out, if it wraps the call to next. Auth belongs before the handler. The body parser belongs before code that reads the body. An error handler has to be in the position that actually catches. A CORS preflight must not require a logged-in user.
why_vibe: The draft copies a stack from a starter and appends auth at the end. Routes above it are public. Or the error middleware is registered last in a framework where last means it never wraps the others.
move: Write the order down: ids, logs, auth, body, handler, errors. Test that an unauthenticated request never reaches the handler. Test that a thrown error hits the error handler.
tell_the_model: Order middleware for (app): request id, logging, CORS, auth, body parser, routes, error handler. Unauthenticated requests must not reach (handler). A CORS preflight must not require auth. A thrown error must hit the error handler.
pitfall: Auth middleware that calls next() after sending 401. The handler runs anyway and may commit.
## mobile
Offline, deep links, safe areas, push, lifecycle, secure storage, background limits. Phone nouns. Not a shrunk website.
### 76 offline-first
id: offline-first
url: https://vibe-coding-101.brighteng.org/c/offline-first/
one_liner: The phone is the source of truth until the network returns. Queue writes. Do not pretend you are online.
what: Offline-first means the app reads and writes a local store, and syncs when it can. The UI shows pending, failed, and conflict states. Online-only with a spinner is a different product.
why_vibe: Models build mobile screens that `fetch` on mount and render nothing in a tunnel. Name offline-first or you shipped a website in a WebView costume.
move: Local read model. A queue of mutations with ids. Sync on reconnect. Surface conflicts instead of last-write-wins by accident. Show pending on the row.
tell_the_model: Offline-first for (feature). Read from local storage. Queue writes with an idempotency key. Sync when online. Show pending and failed. Do not blank the screen when the network fails.
pitfall: A cache of the last GET and no write queue. That is offline-read, and you will lose the user’s edit.
### 77 deep link
id: deep-link
url: https://vibe-coding-101.brighteng.org/c/deep-link/
one_liner: A URL that opens a specific screen, not just the app icon. Cold start must land there too.
what: A deep link maps a URL or universal link to a route inside the app. It must work when the app is dead, backgrounded, or already open. Auth may sit in front, then you still land on the target.
why_vibe: Models add a share button that copies a web URL the app does not claim. Or they handle the link only when the app is already running. Name cold start.
move: One scheme or universal link per entity. Test: kill the app, tap the link, land on the row after login if needed. Do not drop the target on the login redirect.
tell_the_model: Deep link to (screen) via (URL). It must work from a cold start. If login is required, return to the target after auth. Do not only handle the link when the app is already open.
pitfall: A custom scheme nobody registered in the OS project. The link opens the browser and dies.
### 78 safe area
id: safe-area
url: https://vibe-coding-101.brighteng.org/c/safe-area/
one_liner: The notch, the home indicator, the status bar. Content lives inside the inset, not under the glass.
what: Safe area insets are the padding the OS reserves for hardware and system UI. A full-bleed layout must opt in. Text and buttons stay inside the inset.
why_vibe: Models pin a button to `bottom: 0` and a title to `top: 0`. On a real phone both sit under system chrome. Name safe area or the screenshot from the simulator lies.
move: Respect insets on every fixed header and footer. Test on a notched phone and a short phone. Do not hardcode 44px and call it done.
tell_the_model: Respect safe-area insets. No text or button under the notch or home indicator. Headers and footers pad by the inset. Do not hardcode a status-bar height.
pitfall: Padding the whole screen and also the tab bar, so content double-pads and looks shy. Inset once, at the shell.
### 79 navigation stack
id: navigation-stack
url: https://vibe-coding-101.brighteng.org/c/navigation-stack/
one_liner: Screens push and pop. Back goes to the previous screen, not to whatever the model rendered last.
what: A navigation stack is an ordered history of screens. Push adds. Pop removes. Tabs are siblings, not a stack. Modals sit above. Deep links replace or reset the stack on purpose.
why_vibe: Models swap one giant component and call it navigation. The OS back button then exits the app. Name the stack, the tab, and the modal as different things.
move: Pick a navigator. Push for drill-in. Modal for create/edit that must be dismissed. Tabs for top-level sections. Back always pops. Do not remount the world.
tell_the_model: Use a navigation stack. Push (detail). Pop on back. Tabs only for top-level sections. Present (edit) as a modal. Do not toggle screens with a boolean.
pitfall: A stack inside every tab that forgets its state when you switch tabs. Each tab owns its stack.
### 80 push notification
id: push-notification
url: https://vibe-coding-101.brighteng.org/c/push-notification/
one_liner: The OS delivers a message while you are not running. Tapping it is a deep link, not a mystery.
what: A push notification is sent through APNs or FCM to a device token. The app may be dead. The payload should name the destination. Permission is separate from the token, and the token rotates.
why_vibe: Models “add notifications” as an in-app toast. That is not push. Or they send a push with no tap target. Name permission, token, and the deep link.
move: Ask permission in context, not on first launch. Store the device token per user. Payload includes the entity id. Tap opens that screen from cold start. Handle token refresh.
tell_the_model: Real push via (APNs or FCM), not an in-app toast. Ask permission in context. Save the device token. The payload deep-links to (screen). Handle cold start and token refresh.
pitfall: One token per user forever. Reinstalls and second phones exist. Store many tokens, delete dead ones.
### 81 app lifecycle
id: app-lifecycle
url: https://vibe-coding-101.brighteng.org/c/app-lifecycle/
one_liner: Foreground, background, killed. Resume must not assume the screen you left is still valid.
what: The OS moves an app through states: not running, inactive, background, foreground. It may kill you in the background. Timers lie. Network calls you started may return after resume to a dead screen.
why_vibe: Models start a fetch on mount and set state whenever it returns. Background the app, get a warning, or update a screen that unmounted. Name resume and cancellation.
move: Cancel work on blur or unmount. Refresh stale data on foreground if the user was gone. Do not depend on a timer that the OS suspended. Persist drafts before backgrounding.
tell_the_model: Handle app lifecycle. Cancel requests on unmount. Refresh (screen) when returning to foreground. Persist drafts before background. Do not assume a timer kept running.
pitfall: Refreshing the whole app on every resume so the user loses scroll position. Refresh the data, keep the place.
### 82 OS permission
id: os-permission
url: https://vibe-coding-101.brighteng.org/c/os-permission/
one_liner: Camera, photos, location, notifications. Ask when the need is obvious. Survive “don’t allow.”
what: An OS permission is a system prompt the user can deny forever. The app must have a denied state: explain, link to Settings, and still function without that sensor. Asking at launch trains people to refuse.
why_vibe: Models call the camera API on startup and crash or spin when denied. Name the denied state as a screen, not an afterthought.
move: Ask at the gesture that needs it. If denied, show a recovery path. Do not block the rest of the app. Declare the usage string in the OS config or the request never appears.
tell_the_model: Request (permission) only when the user taps (action). If they deny, show an explanation and a way to Settings. The rest of the app still works. Add the OS usage description.
pitfall: Re-prompting in a loop. After a deny, the OS will not show the dialog again. Stop asking. Send them to Settings.
### 83 haptic
id: haptic
url: https://vibe-coding-101.brighteng.org/c/haptic/
one_liner: A small tap from the hardware when something commits. Not a vibration on every pixel.
what: Haptics are tactile feedback: selection, success, warning, impact. They confirm a gesture the screen already showed. They are not a notification, and they are not available on every device.
why_vibe: Models either never use haptics, or buzz on scroll. Name the one moment: the toggle, the destructive confirm, the snap.
move: One haptic on commit, matched to the outcome. Respect reduced-motion / system settings where the platform asks. No haptic on mere hover or scroll.
tell_the_model: Haptic only when (action) commits. Success vs warning, not a buzz on scroll. No-op when the device has no haptics. Do not fire on every list row.
pitfall: A custom vibration pattern that feels like an alarm. Use the platform’s selection and notification styles.
### 84 OTA update
id: ota-update
url: https://vibe-coding-101.brighteng.org/c/ota-update/
one_liner: Ship JS without a store review. You still cannot OTA a native change, and you must be able to roll it back.
also: over-the-air
what: An over-the-air update replaces bundled script or assets on next launch. Store binaries still gate native code, permissions, and entitlements. A bad OTA needs a kill switch.
why_vibe: Models “just publish” after a native module change and expect every phone to update. Name what OTA can and cannot ship, and how you disable a bad bundle.
move: OTA only JS and assets. Native changes go through the store. Pin a runtime version. Keep the previous bundle. A flag or channel lets you halt a bad release.
tell_the_model: This change is (JS-only / native). If native, do not rely on OTA. If JS, ship on a preview channel first. Keep rollback to the previous bundle.
pitfall: OTA on every save to production. You skipped the store and also skipped QA. Use a channel.
### 85 list virtualization
id: list-virtualization
url: https://vibe-coding-101.brighteng.org/c/list-virtualization/
one_liner: Render the rows on screen, not the ten thousand off screen. Recycle views.
what: Virtualization mounts only the visible window of a long list, plus a small overscan, and recycles rows as you scroll. A map of every row mounts every row. Memory and frame time die there.
why_vibe: Models `.map` a feed into views. The simulator has 20 items. A real inbox has 5,000. Name a virtualized list or the phone gets hot.
move: Any list that can exceed a screen uses a virtualized list with a stable key and a known row height when you can. Do not virtualize a form of six fields.
tell_the_model: Virtualize (list). Do not map every row into the tree. Stable keys. Avoid variable-height surprises if you can measure. No virtualization for a short static list.
pitfall: Virtualizing and also fetching every row into memory. The window is the view. The data still needs pagination.
### 97 sync conflict
id: conflict
url: https://vibe-coding-101.brighteng.org/c/conflict/
one_liner: Two edits of the same row while offline. You must pick a rule: merge, reject, or ask.
what: A sync conflict is two writers changing the same record without seeing each other. Last-write-wins is a rule. Field merge is a rule. Asking the user is a rule. Silence is data loss.
why_vibe: Models queue offline writes and POST them later. The second device overwrites the first with no message. Name the rule before the queue exists.
move: Version or updated_at on the row. On push, if the server version moved, apply the rule. Surface a conflict state. Do not drop the local edit on the floor.
tell_the_model: When syncing (object), detect conflicts with a version. If the server changed, (keep server / keep local / ask). Never overwrite silently. Show a conflict state.
pitfall: Last-write-wins on a shared checklist. The other person’s checks vanish. That is a product bug with a technical name.
### 234 secure storage
id: secure-storage
url: https://vibe-coding-101.brighteng.org/c/secure-storage/
one_liner: Tokens live in the keychain or the keystore. AsyncStorage is not a safe.
also: Keychain, Keystore
what: Secure storage is the OS credential store: iOS Keychain, Android Keystore, or EncryptedSharedPreferences backed by it. Items can require biometric unlock. They can survive reinstall depending on the accessibility flag. UserDefaults, SharedPreferences, and AsyncStorage are plaintext files.
why_vibe: The draft saves the refresh token in AsyncStorage because the tutorial did. The backup includes it. A rooted device reads it. The word you needed was keychain.
move: Put credentials and keys in secure storage. Set the accessibility and backup flags on purpose. Do not log the value. Do not mirror it into a database “for convenience.”
tell_the_model: Store (token) in the Keychain or Keystore, not AsyncStorage or UserDefaults. Do not back it up to cloud storage. Do not log it. Require biometric unlock only if (feature) needs it.
pitfall: A keychain item that survives logout because nobody deleted it. The next user on a shared device inherits the session.
### 235 app link
id: app-link
url: https://vibe-coding-101.brighteng.org/c/app-link/
one_liner: An https URL the OS has verified belongs to your app. A custom scheme is not that.
also: universal link, Android App Link
what: An app link is an https URL opened in the app because the domain hosts an association file and the OS checked it: apple-app-site-association, or assetlinks.json. A custom scheme needs no verification, so any app can register it. Verified links do not show a chooser and do not fall through to a lookalike app.
why_vibe: The draft registers myapp:// and calls it a universal link. There is no association file, no associated-domains entitlement, and a cold start still opens Safari.
move: Host the association file on https. Add the entitlement or the intent filter. Test with the app killed. Keep a custom scheme only as a legacy extra, not as the share URL.
tell_the_model: Use a verified app link for (URL). Host the association file. Handle it from a cold start. Do not use a custom scheme as the share link. Do not claim the link works if you only tested with the app already open.
pitfall: A redirect or a login wall on the association file. The OS fetch fails and every link stays in the browser.
### 236 background execution
id: background-execution
url: https://vibe-coding-101.brighteng.org/c/background-execution/
one_liner: The OS suspends your app. A timer is not a background job.
also: BGTask, foreground service
what: Background execution is whatever the OS still lets you do after the user leaves: a scheduled refresh, a processing task, a silent push, an Android foreground service with a visible notification and a declared type. A JavaScript interval stops when the process is suspended. Infinite background work is not a permission you can take.
why_vibe: Generated apps start a setInterval for sync and a WebSocket “so chat stays live.” iOS freezes both a minute after backgrounding. The user thinks sync is broken.
move: Name the OS API that is allowed to run. Finish quickly. On Android, a foreground service needs a notification and a type. Persist the work so a killed process can resume.
tell_the_model: Run (work) with (BGTask or a foreground service), not a timer. It must survive the app being suspended. Show the required notification on Android. Persist progress so a kill can resume.
pitfall: A foreground service with no notification, or the wrong service type. Current Android kills it.
### 237 app extension
id: app-extension
url: https://vibe-coding-101.brighteng.org/c/app-extension/
one_liner: A widget or a share sheet is a separate process with a small memory budget.
also: widget, share extension
what: An app extension runs outside the app: a home-screen widget, a share extension, a watch complication. It does not have the app’s memory, its running state, or its view controllers. Shared files go through an app group. A widget shows a timeline. It is not a live miniature of the app.
why_vibe: The draft imports the app’s root component into the widget and fetches on a timer. The extension is killed for memory, or the widget is blank because it cannot see the app’s in-memory store.
move: A small target. Read a shared store in the app group. Write a timeline. Do not boot the whole app. Do not assume the app is running.
tell_the_model: Build (widget or share extension) as its own target. Read state from the app group, not from the app process. Stay under the memory limit. Do not import the app root.
pitfall: Sharing a database without an app group. The extension and the app are looking at different files.
### 238 adaptive layout
id: adaptive-layout
url: https://vibe-coding-101.brighteng.org/c/adaptive-layout/
one_liner: A tablet is a different layout, not a phone screen stretched.
also: size class, window size class
what: Adaptive layout changes structure with the size class or window size: a tab bar becomes a sidebar, a stack becomes two columns. Scaling the phone UI with a multiplier wastes the tablet and breaks split screen. Rotation and a floating window on Android are size changes, not a separate app.
why_vibe: The generated tablet build is the phone layout with larger type. Split screen overflows. A foldable is untested. The prompt never said size class.
move: Pick compact and regular layouts. Test the smallest phone, a tablet, and split screen. Do not branch on “iPad” as a device name. Branch on the size you were given.
tell_the_model: Adapt (screen) to compact and regular size classes. Use a column on compact and (sidebar or two panes) on regular. Do not scale the phone layout. Do not detect iPad by device name.
pitfall: Reading the screen size once at launch. A split-screen drag never updates the layout.
### 239 in-app purchase
id: in-app-purchase
url: https://vibe-coding-101.brighteng.org/c/in-app-purchase/
one_liner: The store charges the user. Your server decides what that unlocked.
also: StoreKit, Play Billing
what: An in-app purchase goes through StoreKit or Play Billing. The client starts the purchase and shows the pending state. The server verifies the transaction before granting anything. Restore must work on a new phone. A boolean in local storage is not a receipt.
why_vibe: The draft sets premium to true when the button returns. There is no server check, no restore, and no handling for a purchase that is pending parental approval.
move: Server verification. Idempotent grant. Restore on launch. Handle pending, cancelled, and refunded. Do not unlock from the client alone.
tell_the_model: Sell (product) with StoreKit or Play Billing. Verify the transaction on the server before granting it. Support restore. Do not set a local premium flag from the client.
pitfall: Finishing the transaction before the grant is stored. The store will not deliver it again, and the user paid for nothing.
### 240 build variant
id: build-variant
url: https://vibe-coding-101.brighteng.org/c/build-variant/
one_liner: Debug and release are different apps. They do not share a bundle id or a push key.
also: scheme, product flavor, bundle id
what: A build variant is a scheme, flavor, or configuration with its own application id, API host, icons, and signing. Debug installed beside release is a feature. Debug signed with the production push certificate, talking to the production API, is how you notify every customer from a laptop.
why_vibe: The draft has one bundle id and a boolean for the API host. The release build still points at localhost because the flag was compiled wrong. Or the debug build uses the live database.
move: Split bundle ids. Compile the host in. Use separate signing and push credentials. Show a visible mark on non-release builds.
tell_the_model: Add a debug variant of (app) with its own bundle id, API host, and signing. Release must not contain the debug host. Debug must not use the production push certificate.
pitfall: A runtime if that reads an env var from a file the release build still copies in.
### 241 native bridge
id: native-bridge
url: https://vibe-coding-101.brighteng.org/c/native-bridge/
one_liner: UI and native code exchange structured messages. They do not share objects.
also: platform channel, React Native bridge
what: A native bridge, or platform channel, sends JSON-like values between a UI runtime and Kotlin, Swift, or Objective-C. Calls are asynchronous. The UI thread must not block on them. Functions, class instances, and cyclic graphs do not cross. Errors need a code, not only a string.
why_vibe: The draft passes a callback function or a giant object graph across the bridge, or it does file IO on the UI thread inside the native method. The screen freezes, or the call fails with a red box about a non-serializable value.
move: Define a small typed payload. Return results async. Do native work off the UI thread. Map errors to codes the UI handles.
tell_the_model: Expose (action) as a platform channel. Arguments are (shape), JSON-serializable. Do the work off the UI thread. Return a result or an error code. Do not pass functions or class instances.
pitfall: A new channel for every call site, each with a slightly different shape. You cannot tell what the native side accepts.
### 242 dynamic type
id: dynamic-type
url: https://vibe-coding-101.brighteng.org/c/dynamic-type/
one_liner: The user sets a larger text size. The layout has to survive the largest one.
also: Dynamic Type, font scale
what: Dynamic Type, or font scale, is the OS text-size setting. Layouts that fixed a height in points clip the label. Icons that scale without a limit push actions off the screen. The largest accessibility sizes are part of the product, not an edge you skip.
why_vibe: Generated screens use a fixed 16 and a fixed row height. At the largest size the title is cut in half and the button is below the fold with no scroll.
move: Use text styles that scale. Let rows grow. Allow scroll. Test the largest content size. Do not truncate the only label of a control.
tell_the_model: Support dynamic type on (screen) up to the largest accessibility size. Do not fix the row height. Do not clip the label. Keep the primary action reachable.
pitfall: Disabling font scaling on the root to “keep the design.” The setting the user asked for is ignored.
### 243 launch screen
id: launch-screen
url: https://vibe-coding-101.brighteng.org/c/launch-screen/
one_liner: The launch screen is a static picture the OS shows. It is not a place to fetch.
also: splash screen
what: The launch screen is drawn by the OS before your process runs: a storyboard or a splash theme. It cannot load the network, read the user, or animate in your code. The first frame of the app should match it so the handoff does not flash. A spinner you control comes after, if at all.
why_vibe: The draft puts a loading call on the splash and a logo animation. That code does not run yet. Or the first screen is a different color and the app appears to blink twice.
move: A static launch screen that matches the first frame. No text that will be wrong in another language if you can use the logo. Start work after the first real frame.
tell_the_model: Make the launch screen a static match for the first frame of (app). Do not fetch on it. Do not put a spinner in the launch storyboard. Do not flash a second background color.
pitfall: A launch image with “Loading…” baked into the bitmap. It cannot be translated, and it lies when launch is instant.
### 244 biometric
id: biometric
url: https://vibe-coding-101.brighteng.org/c/biometric/
one_liner: The face or fingerprint unlocks a key already on the device. It is not a password you send.
also: Face ID, BiometricPrompt
what: A biometric prompt gates a keychain item or a local key via Face ID, Touch ID, or BiometricPrompt. The server never receives the biometric. A device passcode is the fallback. Changing biometrics can invalidate the item, and the app has to handle that instead of looping the prompt.
why_vibe: The draft sends “biometric: true” to the login API, or stores the user’s face as if it were a secret. There is no passcode fallback, so a failed scan locks the user out.
move: Unlock a local credential with the biometric. Fall back to the device passcode. If enrollment changes, ask the user to sign in again. Do not upload a biometric result as a password.
tell_the_model: Gate (token) with Face ID or BiometricPrompt. Fall back to the device passcode. Do not send the biometric to the server. If biometrics change, require a fresh sign-in.
pitfall: Treating a successful prompt as authentication for the account. It only proved someone could unlock this device.
### 245 background location
id: background-location
url: https://vibe-coding-101.brighteng.org/c/background-location/
one_liner: Location after the app closes is a separate permission, and you have to earn it.
also: always authorization, significant-change location
what: Background location is the right to read position after suspend. iOS splits When In Use from Always. Android splits a foreground permission from background location. Always is for a feature the user can name, such as a recorded trip. Asking for it on first launch is how the review and the user both say no.
why_vibe: The draft requests Always in the first dialog because a map is on the home screen. The purpose string is “to improve your experience.” Review rejects it, or the user denies it and the feature was never going to work.
move: Start with When In Use. Ask for Always only from the feature that needs it, with a sentence that says why. Prefer significant-change updates over a continuous high-accuracy stream.
tell_the_model: Request When In Use location for (feature). Ask for background location only when the user starts (trip). Say why in the purpose string. Do not request Always at first launch. Do not stream high accuracy in the background.
pitfall: A purpose string that does not match the behavior. The OS shows your sentence next to a permission you are abusing.
## desktop
Main process, IPC, signing, windows, trays. A desktop app is not a website in a frame.
### 219 main process vs renderer
id: main-process
url: https://vibe-coding-101.brighteng.org/c/main-process/
one_liner: The shell process owns the files and the OS. The window is a guest.
also: Electron main, Tauri core
what: A desktop shell has a privileged process and a window that draws UI. In Electron the privileged one is the main process and the window is a renderer. In Tauri the core process plays the same role. The window can be compromised like a browser tab. The shell process must not be.
why_vibe: A generated desktop app enables Node in the window “so the UI can read the disk.” One XSS is then a shell. Name which process may touch the filesystem.
move: UI in the window. Files, dialogs, and updates in the shell process. The window asks. It does not import the OS APIs.
tell_the_model: Keep (feature) in the main process. The renderer may only ask over a named IPC channel. Do not enable Node in the window. Do not import fs in UI code.
pitfall: A preload script that re-exports the entire Node API into the page.
### 220 IPC
id: ipc
url: https://vibe-coding-101.brighteng.org/c/ipc/
one_liner: A named channel between the window and the shell. Not a function that runs anything.
also: inter-process communication, contextBridge
what: IPC is a message between processes. The window sends a channel name and structured arguments. The shell handles that channel and returns a result. A generic “invoke this string” channel is remote code execution with extra steps.
why_vibe: The usual draft exposes `ipcRenderer.invoke(channel, ...args)` to the page, then switches on the channel in one giant handler. Any script in the page can call any channel.
move: One function per action, registered in a preload with contextBridge. Validate arguments in the shell. No channel name that comes from the page as data.
tell_the_model: Expose only (action) from the preload. Arguments are (shape). Validate them in the main process. Do not pass a channel name or a function from the renderer.
pitfall: Sending the whole file path and trusting it. The page can point that path at secrets.
### 221 context isolation
id: context-isolation
url: https://vibe-coding-101.brighteng.org/c/context-isolation/
one_liner: The page cannot see Node, even if it is compromised.
also: nodeIntegration off, contextBridge
what: Context isolation runs the page and the preload in separate JavaScript worlds. nodeIntegration stays off. The preload exposes a small object with contextBridge. Without isolation, a cross-site script in the window can require the filesystem.
why_vibe: Templates turn isolation off because a tutorial from 2018 did, or because a library “needs Node.” The demo works. The threat model is gone.
move: contextIsolation true, nodeIntegration false, sandbox true. Expose methods, not modules. Remote content gets a separate window with no bridge.
tell_the_model: Enable context isolation and disable nodeIntegration for (window). Expose (methods) through contextBridge only. Do not turn isolation off to unblock a library.
pitfall: A “dev only” exception that the production build still ships.
### 222 single instance
id: single-instance
url: https://vibe-coding-101.brighteng.org/c/single-instance/
one_liner: The second launch focuses the window that is already open. It does not start another copy.
also: single instance lock
what: A single-instance lock makes the second process exit and hand its arguments to the first: a file to open, a protocol URL, a deep link. Document apps and menu-bar apps need this. Two copies fight over the same files and the same port.
why_vibe: Generated apps ignore the second launch. The user double-clicks a file and gets a second window that does not know about the first, or a port collision on the local helper.
move: Take the lock at startup. On the second instance, focus the existing window and forward argv. If the lock is lost, quit.
tell_the_model: Enforce a single instance. If the app is already running, focus it and deliver (file or URL) to that process. Do not open a second window or a second local server.
pitfall: Taking the lock after the window opens. Two windows flash before one dies.
### 223 code signing
id: code-signing
url: https://vibe-coding-101.brighteng.org/c/code-signing/
one_liner: The OS checks who signed the binary. Unsigned apps are blocked, not just warned, on current systems.
also: notarization, Gatekeeper, Authenticode
what: Code signing attaches an identity to the app. macOS then wants notarization and a stapled ticket so Gatekeeper can check it offline. Windows uses Authenticode so SmartScreen is not a brick wall. Auto-update only works if the new build is signed by the same identity.
why_vibe: A generated release is a zip of an unsigned binary. It runs on the author machine and fails on the first other Mac. The fix attempted in chat is “right-click, Open,” which is not a release.
move: Sign every build you hand to someone else. Notarize macOS builds. Use the same certificate the updater expects. Do not commit the certificate or the password.
tell_the_model: Sign (app) with the release identity and notarize the macOS build. Staple the ticket. Do not disable Gatekeeper. Do not put the certificate in the repo.
pitfall: Signing the app and not the nested helpers, frameworks, or the updater. One unsigned piece fails the check.
### 224 desktop auto-update
id: desktop-update
url: https://vibe-coding-101.brighteng.org/c/desktop-update/
one_liner: Download a signed build, check it, and swap it on quit. Do not overwrite a running binary.
also: Sparkle, Squirrel, electron-updater
what: A desktop updater fetches a manifest, downloads a package signed by the same identity, and applies it when the app quits or on the next launch. Windows cannot replace a running exe in place. A failed apply must leave the old version runnable.
why_vibe: The draft downloads a zip over HTTP and unpacks it on top of the install. There is no signature check and no way back. Or it tells the user to reinstall from a website.
move: HTTPS manifest. Signature check before apply. Apply on quit. Ship a staged channel. Keep the previous version until the new one has started once.
tell_the_model: Auto-update (app) from a signed feed. Verify the signature before replacing files. Apply on quit, not over the running binary. If the new version fails to start, keep the old one.
pitfall: An update URL that is a plain string the window can change. The page then points the updater at an attacker.
### 225 protocol handler
id: protocol-handler
url: https://vibe-coding-101.brighteng.org/c/protocol-handler/
one_liner: The OS opens your app for a scheme or a file type. The already-running instance must receive it.
also: file association, custom URL scheme
what: A protocol handler registers a URL scheme or a file extension. The OS launches the app, or forwards the URL to the running instance. The installer has to write that registration. A handler that only works from inside a running debug session is not registered.
why_vibe: Generated apps listen for the URL in the window after load. Cold start from a double-clicked file drops the path. Windows delivers it on the command line of the second process, which the app ignores.
move: Register the scheme and the file types in the package. Handle them on cold start and on the second instance. Parse the URL. Do not treat it as a shell command.
tell_the_model: Register (scheme or extension) for (app). On cold start and on a second launch, open that target. Do not shell out to the URL. Do not drop it if the app was not already open.
pitfall: A custom scheme with no host allowlist. Any page can launch the app and pass it arguments.
### 226 window state
id: window-state
url: https://vibe-coding-101.brighteng.org/c/window-state/
one_liner: Remember where the window was. If that display is gone, open it on a display that exists.
also: window bounds, multi-monitor
what: Window state is position, size, and whether it was maximized or full screen, stored per display. Restoring a coordinate from an unplugged monitor puts the window off-screen. The user thinks the app did not start.
why_vibe: The draft saves x and y and restores them blindly. After a laptop undocks, the window is on a display that is not there. There is no menu command that can find it.
move: Save bounds on move and resize. On launch, intersect them with the current displays. If they miss, use a default on the primary display. Do not persist a maximized size as a normal width.
tell_the_model: Restore the window bounds for (app). If the saved display is gone, open on the primary display at a default size. Do not restore an off-screen window.
pitfall: Saving state on every mousemove without a debounce, and writing a file on the UI thread.
### 227 tray
id: tray
url: https://vibe-coding-101.brighteng.org/c/tray/
one_liner: Closing the window does not quit a tray app. Quitting is a menu item.
also: menu bar extra, system tray, NSStatusItem
what: A tray or menu-bar app keeps running with no window. The icon has a menu. Closing the last window hides it. Quit is explicit. On macOS a menu-bar-only app usually does not appear in the Dock. Linux tray support depends on the desktop session.
why_vibe: The generated app quits when the window closes, so the tray icon dies with it. Or it stays running and the user has no way to quit except the activity monitor.
move: Decide: document app quits on last window, tray app does not. Put Quit and Show in the tray menu. On macOS, set the activation policy to match.
tell_the_model: (App) is a tray app. Closing the window hides it and leaves the process running. The tray menu has Show and Quit. Do not quit when the last window closes.
pitfall: A tray icon with no menu and no keyboard way to quit. Also a Dock icon plus a tray, so the user sees two apps.
### 228 native dialog
id: native-dialog
url: https://vibe-coding-101.brighteng.org/c/native-dialog/
one_liner: Open and save go through the OS dialog. The path it returns is the permission.
also: open panel, save panel
what: A native open or save dialog is an OS panel. It returns a path the user chose. On a sandboxed Mac that path is a security-scoped bookmark you must keep if you want to reopen the file later. An HTML file input in a desktop window does not grant that, and it looks like a website.
why_vibe: The draft puts `` in the window and then tries to read an arbitrary path. The sandbox denies it, or the app asks for broad disk access instead of the one file.
move: Show the dialog from the shell process. Remember a bookmark if you must reopen. Do not request full-disk access because the dialog felt inconvenient.
tell_the_model: Use the native open dialog for (document). Start in the shell process. Persist access with a security-scoped bookmark if the sandbox requires it. Do not use an HTML file input. Do not ask for full disk access.
pitfall: Holding the path string and assuming it is still readable after relaunch. The sandbox grant died with the process.
### 229 app sandbox
id: app-sandbox
url: https://vibe-coding-101.brighteng.org/c/app-sandbox/
one_liner: The OS allowlist for a desktop app. Entitlements are the permission, not a comment.
also: entitlements, App Sandbox
what: An app sandbox limits files, network, camera, and other OS resources to what the entitlements name. macOS App Store apps must be sandboxed. A temporary-exception entitlement is a hole you have to justify. User-selected files are allowed one at a time, not as a home-directory grant.
why_vibe: Generated Mac apps turn the sandbox off, or add com.apple.security.files.user-selected.read-write plus a home-directory exception “so saving works.” Review rejects it, or the unsandboxed build reads the whole disk.
move: Start sandboxed. Add the smallest entitlement that the feature needs. Use the open dialog and bookmarks for documents. Do not grant the home directory.
tell_the_model: Sandbox (app). Allow only (entitlement) for (feature). Use security-scoped bookmarks for user files. Do not disable the sandbox. Do not add a home-directory exception.
pitfall: Copying an entitlements file from a gist that includes debugging exceptions, then shipping it.
### 230 webview
id: webview
url: https://vibe-coding-101.brighteng.org/c/webview/
one_liner: A browser inside the app. Remote pages do not get the app’s files or IPC.
also: WKWebView, WebView2
what: A webview embeds web content in a native window. Your own UI can have a narrow bridge. A webview that loads arbitrary URLs is a browser: no Node, no broad IPC, no shared cookie jar with the shell unless you mean it. WKWebView and WebView2 are the OS controls. An old Electron `` tag with node is the dangerous one.
why_vibe: The draft loads the marketing site, the docs, or user-supplied HTML in the same window as the app and shares the preload. The remote page can then call the file bridge.
move: Split origins. App UI gets the bridge. Remote content gets a separate webview with no bridge, no node, and a locked navigation handler.
tell_the_model: Load (remote URL) in a webview with no Node and no IPC. Do not share the app preload or cookies with it. Block navigation off (origin).
pitfall: Disabling web security so a local file can call an API. That flag removes the boundary you needed.
### 231 app packaging
id: app-packaging
url: https://vibe-coding-101.brighteng.org/c/app-packaging/
one_liner: The installer is the product the OS sees. A folder of files is not an install.
also: DMG, MSI, MSIX, AppImage
what: Packaging builds the artifact the OS installs: a signed app bundle in a DMG or PKG, an MSI or MSIX, an AppImage or deb. Each has its own update path and its own place for icons, file associations, and uninstall. asar or a similar archive hides source from a casual glance. It does not hide secrets, and it is not a signature.
why_vibe: The release instruction is “zip the project and send it.” There is no uninstall, no file association, and the user runs it from Downloads forever. Secrets in the bundle ship with it.
move: One package per OS. Put metadata, icons, and associations in the package. Keep secrets out. Test install, update, and uninstall on a clean machine.
tell_the_model: Package (app) as (DMG or MSI or AppImage). Include the icon, file associations, and an uninstaller. Do not zip the repo. Do not embed secrets in the bundle.
pitfall: A different app id in the package than the one the updater and the keychain expect. You shipped a second app.
### 232 global shortcut
id: global-shortcut
url: https://vibe-coding-101.brighteng.org/c/global-shortcut/
one_liner: A global hotkey fires even when another app is focused. An in-window shortcut does not.
also: global hotkey, accelerator
what: A global shortcut is registered with the OS and works from any app. It fails if another app already owns that chord. An accelerator on a menu only works when your window is focused. They are different registrations. A global shortcut must be released on quit.
why_vibe: The draft registers Cmd-C or Ctrl-Space globally and steals it from every other app. Or it registers a hotkey and never unregisters, so the next launch cannot take it.
move: Prefer a menu accelerator. Register a global shortcut only for the one action that must work in the background. Detect failure. Unregister on quit. Let the user change it.
tell_the_model: Bind (action) as a menu accelerator, not a global hotkey. If it must be global, register (chord), fail visibly if it is taken, and unregister on quit. Do not bind copy, paste, or space.
pitfall: Registering the shortcut in the renderer. The OS registration belongs in the shell process, once.
### 233 native menu
id: native-menu
url: https://vibe-coding-101.brighteng.org/c/native-menu/
one_liner: Use the OS menu bar. Roles give you undo, quit, and the window menu for free.
also: application menu, menu role
what: A native menu is the OS menu bar or the window menu, with roles the platform already implements: undo, cut, paste, hide, quit, minimize. An HTML menu inside the page does not get those commands, the OS services menu, or the keyboard behavior users expect.
why_vibe: Generated Electron apps ship the default menu, or delete it and draw a div. Quit is missing on Windows. Undo does not reach native text fields. The user cannot find Preferences where the platform puts it.
move: Set the application menu from the shell. Use roles for standard items. Put app-specific items next to them. Mirror the ones that matter in the tray if you have one.
tell_the_model: Use the native application menu for (app). Keep the undo, cut, copy, paste, window, and quit roles. Add (items) beside them. Do not replace the menu with HTML.
pitfall: Removing the menu to look like a website. You also removed Quit and the edit commands.
## game
Game loop, delta time, collision, sprite sheets, object pools. Name them or the model invents a slideshow.
### 86 game loop
id: game-loop
url: https://vibe-coding-101.brighteng.org/c/game-loop/
one_liner: Input, update, render, repeat. Not a chain of setTimeouts that drift.
what: The game loop is the heartbeat: read input, step the simulation, draw. It runs on a frame callback. Update and render are separate so you can step the sim without drawing, and draw without changing the sim.
why_vibe: Models animate with CSS or a pile of timers. Collisions then depend on frame rate. Name the loop: one place that advances the world.
move: One `requestAnimationFrame` or engine tick. Fixed order: input, update(dt), render. No gameplay in a timer beside the loop.
tell_the_model: One game loop. Order: input, update, render. Do not use setTimeout or CSS to move gameplay objects. Keep drawing separate from the simulation step.
pitfall: Two loops, one for UI and one for “enemies,” that do not share time. One clock.
### 87 delta time
id: delta-time
url: https://vibe-coding-101.brighteng.org/c/delta-time/
one_liner: Move by time, not by frames. A slow machine must not make the jump shorter.
what: Delta time is the seconds since the last tick. Velocities are units per second, multiplied by dt. A spike in dt (tab backgrounded) must be clamped or the character tunnels through the wall.
why_vibe: Models do `x += 5` per frame. At 30fps the game is half speed. At 144fps it is a blur. Say per-second and clamp dt.
move: All motion is `speed * dt`. Clamp dt to a max (one or two frames). Pause sets dt to 0. Do not accumulate uncapped time after a stall.
tell_the_model: Use delta time. Speeds are units per second. Clamp dt so a hitch does not teleport. Pausing must pass dt = 0. Do not move by a constant per frame.
pitfall: Clamping so hard that a long frame drops time and the sim falls behind forever. Cap, and if you must, run a few fixed steps to catch up — with a limit.
### 88 entity component system
id: ecs
url: https://vibe-coding-101.brighteng.org/c/ecs/
one_liner: Entities are ids. Components are data. Systems are the functions. Do not build a God class named Player.
also: ECS
what: ECS stores behaviour as data on ids and runs systems over the entities that have the right components. Composition, not a Player subclass of Character subclass of Entity.
why_vibe: Models make `class Player extends Sprite` and bolt on inventory, AI, and UI. The next feature subclasses again. For anything beyond a toy, say ECS or say composition — and do not say both vaguely.
move: If the game has many similar objects, use components: position, velocity, sprite, health. Systems iterate. A player is an entity with a player tag, not a subclass.
tell_the_model: Use ECS. Entities are ids. Components are plain data: position, velocity, sprite. Systems update them. Do not make a Player class hierarchy.
pitfall: An “ECS” where every system special-cases the player. Then you have a player class in denial.
### 89 collision
id: collision
url: https://vibe-coding-101.brighteng.org/c/collision/
one_liner: Detect overlap, then resolve it. Detection without a response is a highlight, not physics.
what: Collision detection asks whether shapes overlap. Resolution separates them or fires gameplay (damage, pickup, bounce). Layers decide who checks whom: player vs wall, bullet vs enemy, not everything vs everything.
why_vibe: Models check bounding boxes and then set a boolean. The sprite stays inside the wall. Or they check every pair every frame. Name layers and the response.
move: Shapes, not pixels, unless you mean it. Layers or masks. Resolve penetration for solids. Triggers report and do not push. Broad phase if the count is high.
tell_the_model: Collide (A) with (B) using (shape). Solids resolve penetration. Triggers only report. Use layers so (A) does not collide with itself. Do not pair every object with every object.
pitfall: Resolving by teleporting to the last safe pixel with no slide. Corners stick. Separate axes or let a known solver do it.
### 90 sprite sheet
id: sprite-sheet
url: https://vibe-coding-101.brighteng.org/c/sprite-sheet/
one_liner: Many frames in one image. The game draws a rectangle, not a new file per frame.
what: A sprite sheet packs frames and poses into one texture. An atlas map says which rectangle is “run-3”. One draw call batch beats a hundred image files.
why_vibe: Models emit fifty `` tags or fifty files. Memory and loading stutter. Name the sheet, the frame size, and the animation names.
move: One sheet per character or tileset. Constant frame size unless the atlas says otherwise. Animations are lists of frame indexes and durations. Do not load a PNG per frame.
tell_the_model: Use a sprite sheet for (character). Frame size (w×h). Animations: idle, run, with frame indexes and durations in ms. Draw a source rectangle. Do not use one file per frame.
pitfall: A sheet with no padding, so the GPU bleeds the next frame into this one. Leave a pixel, or extrude edges.
### 91 state machine
id: state-machine
url: https://vibe-coding-101.brighteng.org/c/state-machine/
one_liner: Explicit states and the only legal transitions. Idle can go to run. Dead cannot go to jump.
what: A finite state machine is a set of states and allowed edges. Events trigger transitions. Each state owns enter, update, and exit. A pile of booleans (`isJumping && !isDead && isAngry`) is not a machine.
why_vibe: Models add booleans. Combinations explode and an impossible pose ships. Name the states and the forbidden edges.
move: List states. List events. Write the table. Illegal events no-op. On transition, exit the old and enter the new. Animations key off the state, not off a parallel boolean.
tell_the_model: Model (actor) as a state machine: (states). Only these transitions: (edges). Illegal events do nothing. Do not use a pile of booleans.
pitfall: A machine that any state can reach any other “for flexibility.” Then you do not have a machine. You have a suggestion.
### 92 object pool
id: object-pool
url: https://vibe-coding-101.brighteng.org/c/object-pool/
one_liner: Reuse bullets and particles. Allocating every shot is how the frame hitch arrives.
what: An object pool preallocates N objects and hands out inactive ones. Release returns them. The point is stable memory and no garbage spikes, not “elegance.”
why_vibe: Models `spawn = new Bullet()` in the fire handler. At a high fire rate the garbage collector stutters the frame. Name the pool and the cap.
move: Pool anything spawned many times a second: bullets, particles, damage numbers. Fixed cap. If the pool is empty, refuse or recycle the oldest. Do not grow without a limit.
tell_the_model: Pool (objects). Preallocate (N). Take an inactive one on spawn, release on death. If none are free, recycle the oldest or drop the spawn. Do not allocate per shot.
pitfall: A pool that still allocates when empty “just this once.” That once is the hitch you were avoiding.
### 93 hitbox and hurtbox
id: hitbox
url: https://vibe-coding-101.brighteng.org/c/hitbox/
one_liner: Where you deal damage, and where you take it. They are not the sprite’s rectangle.
what: A hitbox is the volume that offends. A hurtbox is the volume that receives. They differ in size and timing: a sword’s hitbox exists only on the active frames. The sprite is art, not the collision.
why_vibe: Models collide the whole image. Jumps feel unfair and attacks feel random. Name the two boxes and the frames they exist on.
move: Author hurtboxes on the body. Author hitboxes on the attack, enabled only during active frames. Draw them in debug. Tune them, do not use the PNG bounds.
tell_the_model: Separate hitbox and hurtbox from the sprite. Hurtbox on the body. Hitbox only during active attack frames. Add a debug draw. Do not collide using the image rectangle.
pitfall: One box for both, so you punch yourself. Layers: offense does not hit your own hurtbox unless that is the joke.
### 94 camera follow
id: camera-follow
url: https://vibe-coding-101.brighteng.org/c/camera-follow/
one_liner: The camera eases toward the player. It does not glue to their pixels, and it does not show past the level.
what: A follow camera tracks a target with damping, a look-ahead in the facing direction, and bounds so the view stays inside the map. Cutscenes take control and give it back.
why_vibe: Models set `camera = player.position` every frame. The screen vibrates with the walk cycle and shows void past the map. Name damping and bounds.
move: Lerp or critically damp toward the target. Offset by facing. Clamp to the level. Dead zone so tiny motion does not nudge the frame. Shake is a separate, short-lived offset.
tell_the_model: Camera follows (target) with damping, a look-ahead on facing, and bounds inside the level. Do not lock the camera to the sprite every frame. Do not show outside the map.
pitfall: Smoothing so heavy the player can walk off-screen. The target stays in a central safe region.
### 95 save game
id: save-game
url: https://vibe-coding-101.brighteng.org/c/save-game/
one_liner: Serialize the sim, not the sprites. Version the blob. Load must survive an old file.
what: A save is a versioned snapshot of gameplay state: positions, flags, inventory, seed. It is not a screenshot and not the live object graph. Loading migrates old versions forward.
why_vibe: Models `JSON.stringify` the entire scene, including functions and GPU handles, or they forget a version. Next patch, every save breaks. Name the schema and the migration.
move: A plain data schema with a version integer. Save on a deliberate beat and on pause. Load constructs the world from data. Write a migration per version step. Never trust the file’s shape blindly.
tell_the_model: Save (state) as versioned plain data, not live objects. Include version. On load, migrate older versions forward. Autosave on pause. Do not stringify functions, textures, or the whole scene graph.
pitfall: Saving mid-frame with half-updated components. Save at a safe point: after update, before the next input.
### 96 frame budget
id: frame-budget
url: https://vibe-coding-101.brighteng.org/c/frame-budget/
one_liner: A 60fps frame has about 16ms. If you spend 20, you are not at 60. Measure before you decorate.
what: Frame budget is the time you may spend per tick for the target rate. 60fps is ~16.7ms, 30fps ~33ms. Update, physics, and render share it. A spike drops a frame.
why_vibe: Models add lights, particles, and full-screen effects until the tab fans the laptop. They never looked at a timer. Name the budget and a counter.
move: Pick 60 or 30 and say it. Time update and render separately in debug. If you are over, cut draw calls or pool spawns before adding content. Do not “optimize” blind.
tell_the_model: Target (60 or 30) fps. Show update ms and render ms in a debug counter. Do not add effects that blow the frame. Pool spawns. Batch draws.
pitfall: Reading the counter every frame in a way that itself costs a frame. Sample, do not allocate strings in the hot path.
## system design
Load balancers, shards, replicas, eventual consistency, sagas, canaries. The names that show up once one machine is not enough.
### 98 load balancer
id: load-balancer
url: https://vibe-coding-101.brighteng.org/c/load-balancer/
one_liner: One address, many machines. It spreads requests and stops sending them to a dead one.
what: A load balancer sits in front of identical servers and picks one per request. Health checks pull a bad instance out of rotation. Sticky sessions pin a user to one machine — useful, and a trap if that machine holds the only copy of their state.
why_vibe: Models “scale” by making the server faster, or they put session state in memory and then add a second instance that cannot see it. Name the balancer and say where state lives.
move: Stateless app servers behind a balancer. Session and uploads live in a shared store. Health check hits a real ready route. Do not pin everyone to one box unless you must.
tell_the_model: Put (service) behind a load balancer. Servers are stateless. Session and files live in a shared store. Unhealthy instances leave rotation. Do not keep user state only in process memory.
pitfall: A balancer in front of one server “for later.” You added a hop and no capacity. Add it when the second instance exists.
### 99 horizontal scaling
id: horizontal-scaling
url: https://vibe-coding-101.brighteng.org/c/horizontal-scaling/
one_liner: More machines, not a bigger one. That only works if no single process owns the user.
also: scale out, not up
what: Vertical scaling buys a larger box. Horizontal scaling adds boxes. Horizontal demands stateless workers, shared data, and a way to route. A bigger box fails as one piece.
why_vibe: Models answer “make it scale” with a larger instance type and a comment. The next limit is the same shape, just later. Say scale out and name the shared state.
move: List what is in memory: sessions, caches, locks, uploads. Move those out. Then a second instance must pass the same tests. One machine dying must not drop everyone.
tell_the_model: Scale (service) horizontally. No per-user state in process memory. Two instances must behave the same. Killing one must not lose writes that were acknowledged.
pitfall: Ten containers sharing one SQLite file on one disk. You multiplied the app and kept a single data choke.
### 100 CDN
id: cdn
url: https://vibe-coding-101.brighteng.org/c/cdn/
one_liner: Static bytes cached near the user. HTML that changes per person usually does not belong there.
what: A content delivery network caches responses at the edge. It wins for images, scripts, fonts, and public pages with a clear TTL. Personalized HTML and authenticated APIs are a different problem — caching those wrong leaks one user to another.
why_vibe: Models slap “add a CDN” on the whole origin. The next bug is a cached logged-in page. Name what is public, the cache key, and who purges it.
move: Fingerprinted assets: long cache, immutable. HTML: short or bypass if it varies by cookie. Never vary a shared cache on nothing while the body depends on the user.
tell_the_model: CDN only for public (assets or pages). Cache key must include whatever changes the body. Do not cache responses that depend on a cookie unless the key includes it. Purge on publish.
pitfall: Caching `/api/me` at the edge. One user’s profile becomes the world’s.
### 101 CAP theorem
id: cap
url: https://vibe-coding-101.brighteng.org/c/cap/
one_liner: During a partition you choose: answer with stale or different data, or stop answering. You do not get both.
what: CAP says a distributed store, while the network is split, cannot be both consistent and available. Consistent means every read sees the latest write. Available means every request gets a response. Outside a partition you can have both. The theorem is about the split.
why_vibe: Models promise “strongly consistent and always up, globally.” That sentence is the bug. Make them pick a side for the partition and write it down.
move: For each datum, say what a user sees if a region cannot talk to the other: error, or a stale read. Payments and inventory usually prefer the error. Feeds can prefer stale.
tell_the_model: State the CAP choice for (data). During a network partition, either reject writes/reads or serve stale. Do not claim strong consistency and full availability across regions.
pitfall: Quoting CAP to avoid indexes and transactions on one database. One node is not a partition story.
### 102 replication
id: replication
url: https://vibe-coding-101.brighteng.org/c/replication/
one_liner: Copies of the same data on more than one node. Copies drift unless you say how writes propagate.
what: Replication keeps a copy so you can survive a dead disk and sometimes serve reads closer to the user. Synchronous replication waits for copies before acknowledging. Asynchronous replication acknowledges early and can lose the last writes.
why_vibe: Models add a “replica” in a diagram and acknowledge writes from memory. On failover those writes vanish. Name sync vs async and what failover promotes.
move: Writes go to a primary. Replicas follow. If you ack only after the sync replica, failover can be lossless. If you ack early, say what the user can lose.
tell_the_model: Replicate (store). Writes hit a primary. Acknowledge only after (sync replica / local commit — pick one and name the loss). Failover must promote a replica, not invent a new empty node.
pitfall: Reading from a lagging replica and treating it as the write you just did. The user saves, refreshes, and the row is gone for a second. Route that read to the primary.
### 103 sharding
id: sharding
url: https://vibe-coding-101.brighteng.org/c/sharding/
one_liner: Split rows across databases by a key. The key is the product decision. A bad key is a hot shard.
what: A shard is a partition of the data: each node owns a slice, not a full copy. You shard when one machine cannot hold or write the set. Queries that need many shards become scatter-gather. Resharding moves rows and hurts.
why_vibe: Models shard on day one “for scale,” or they shard by a random id and then every inbox fans out to every node. Name the key and the query that must stay on one shard.
move: Shard only when one node’s disk, write rate, or backup window is the proven limit. Pick a key you filter by (tenant, not a global timestamp). Keep transactions inside one shard.
tell_the_model: Shard (table) by (tenant or user id), not by a random key. Queries for one tenant hit one shard. Do not start with shards if one database still fits. No cross-shard transaction in this slice.
pitfall: A shard per customer when most customers are tiny and one is huge. The huge one is still a hot shard. You needed a different split, or a whale on its own node.
### 104 consistent hashing
id: consistent-hashing
url: https://vibe-coding-101.brighteng.org/c/consistent-hashing/
one_liner: Adding a node moves only some keys, not all of them. That is the point of the ring.
what: Consistent hashing maps keys and nodes onto a ring. A key lives on the next node clockwise. Adding or removing a node remaps only the keys that walked past it, not the entire table. Virtual nodes keep the spread even.
why_vibe: Models “rebalance” by `hash(key) % N`. Change N and every key moves. Caches empty. Databases rewrite. Say consistent hashing if the set of nodes changes.
move: Use it for caches and for shard maps that grow. Include virtual nodes. On a miss after a topology change, expect one extra fetch, not a full flush.
tell_the_model: Map (keys) to nodes with consistent hashing, not hash % N. Adding a node must move only a fraction of keys. Use virtual nodes so one real node is not a hotspot.
pitfall: A ring with three physical nodes and no virtual nodes. One node gets half the keys. The picture looked fair. The histogram is not.
### 105 read replica
id: read-replica
url: https://vibe-coding-101.brighteng.org/c/read-replica/
one_liner: Copies that serve reads. They lag. A read-your-writes screen must not use them.
what: A read replica follows the primary and answers SELECTs. It takes read load off the primary. It is seconds or milliseconds behind. Writes still go to the primary. Replication lag is normal, not an incident, until a user can see it.
why_vibe: Models send every query to the replica “to scale reads,” including the SELECT right after INSERT. The UI says the save failed. Name lag and the exception path.
move: Lists and reports can use the replica. The request that just wrote reads the primary, or waits for the replica to catch that id. Show lag in metrics.
tell_the_model: Send read-heavy (lists) to a read replica. Anything that just wrote, or must be read-your-writes, hits the primary. Do not treat replica lag as a bug to hide. Measure it.
pitfall: A replica in another region used for a checkout balance. The user spends money the primary already knows they do not have.
### 106 eventual consistency
id: eventual-consistency
url: https://vibe-coding-101.brighteng.org/c/eventual-consistency/
one_liner: If writes stop, copies agree. Until then they may disagree. The UI has to admit that.
what: Eventual consistency means replicas converge without a promise about when. Strong consistency means a read sees the latest acknowledged write. “Eventual” is a product behavior: the like count, the search index, the other phone.
why_vibe: Models store denormalized counts and read them as truth. Two taps race and the number is wrong forever, not just briefly. Say what is allowed to be stale and what converges.
move: Mark each read model: stale-ok or not. Stale-ok gets a replica or a projection. Not-stale stays on the primary and a transaction. Do not mix them in one sentence.
tell_the_model: (View) is eventually consistent. Stale reads are allowed for (seconds). (Balance or permission) is not. Do not read the projection to authorize a write.
pitfall: Using “eventual” as an excuse for a bug that never converges. If writes have stopped and it is still wrong, that is a broken projection, not CAP.
### 107 saga
id: saga
url: https://vibe-coding-101.brighteng.org/c/saga/
one_liner: A multi-step business action with a compensating step for each. Not a distributed transaction you hoped would exist.
what: A saga is a sequence of local transactions across services. Each step has an undo. If step three fails, you run the undos for two and one. There is no global lock. The user can observe the in-between states.
why_vibe: Models wrap “charge, reserve stock, email” in one fictional transaction across three APIs. The second call fails and the charge remains. Name the saga and the compensation.
move: Write the steps and the undo for each. Persist which step you are on. Retries must be idempotent. If an undo can fail, that is an operator alert, not a log line.
tell_the_model: Implement (flow) as a saga, not one transaction across services. Each step has a compensation. Persist progress. Retries are idempotent. If compensation fails, surface it. Do not pretend two APIs share a commit.
pitfall: A saga with no undo for the payment step. You automated the forward path and left refunds as a ticket queue.
### 108 transactional outbox
id: outbox
url: https://vibe-coding-101.brighteng.org/c/outbox/
one_liner: Write the row and the “please publish this event” in the same database transaction. A worker publishes after.
what: The outbox pattern avoids the dual write: you commit business data and an outbox row together. A separate process reads the outbox and publishes to the queue, then marks it sent. Crash between commit and publish no longer loses the event.
why_vibe: Models commit the order, then await the queue. The process dies in the gap. The order exists and nobody is told. Name the outbox or that gap stays.
move: Same transaction: domain write plus outbox insert. Publisher is idempotent. Consumers are idempotent. Do not publish inside the database transaction.
tell_the_model: Use a transactional outbox for (event). Insert it in the same transaction as the (row). A worker publishes after commit. Do not call the broker inside the transaction. Consumers must be idempotent.
pitfall: An outbox nobody polls. The table grows and the event never leaves. The worker is part of the design, not a follow-up.
### 109 cache stampede
id: cache-stampede
url: https://vibe-coding-101.brighteng.org/c/cache-stampede/
one_liner: The hot key expires and every request rebuilds it at once. One recomputation. The rest wait or get stale.
also: thundering herd
what: A stampede is a rush of identical work when a cache entry disappears. A thundering herd is the same pile-on against a database, a lock, or a cold service. One expensive query becomes a thousand.
why_vibe: Models cache with a TTL and no lock. At expiry the database sees the whole fleet. Say single-flight or early refresh.
move: One worker rebuilds the key. Others wait or serve slightly stale. Refresh before expiry for hot keys. Jitter the TTL so keys do not die together.
tell_the_model: Prevent a stampede on (key). Only one request recomputes it. Others wait or serve stale. Jitter TTLs. Do not let every instance miss and query at once.
pitfall: A lock that never unlocks when the rebuilder dies. The key stays missing. Put a deadline on the lock.
### 110 hot key
id: hot-key
url: https://vibe-coding-101.brighteng.org/c/hot-key/
one_liner: One key gets a disproportionate share of traffic. The shard that owns it falls over. The others look idle.
what: A hot key is a partition, cache entry, or row that attracts far more traffic than its neighbors. Celebrity accounts, a global counter, “latest” sorted by one timestamp. The node is busy. The cluster graph looks fine.
why_vibe: Models shard evenly on paper and store a single `stats:global` key. Launch day that key melts one node. Name the hot key before you pick the shard key.
move: Find keys that are not uniform. Split them: per-shard counters you sum, or replicate a read-only copy. Do not hash a key everyone shares and expect balance.
tell_the_model: Assume (key) is hot. Do not put all of its traffic on one shard. Split counters or replicate the read. Do not use a single global key for a write on every request.
pitfall: Adding shards without changing the key. The hot key still hashes to one of them. You bought nodes and kept the bruise.
### 111 backpressure
id: backpressure
url: https://vibe-coding-101.brighteng.org/c/backpressure/
one_liner: When the consumer is slow, the producer must slow down or shed. An unbounded queue is a delayed outage.
what: Backpressure propagates “I am full” toward the sender. Bounded queues, blocked writes, or 429s. Without it, producers keep accepting work the system cannot do, and memory grows until the process dies.
why_vibe: Models add a queue “so spikes are fine” and leave it unbounded. The spike becomes a backlog that takes down the box hours later. Name the bound and the behavior when it is hit.
move: Bound every queue. When full: reject, or block the producer, on purpose. Measure depth. A growing depth is the page, not a badge.
tell_the_model: Bound the queue for (work). When it is full, reject new work with a clear error. Do not grow memory without a limit. Expose queue depth.
pitfall: Dropping the oldest job silently. You applied pressure by losing the one task someone is waiting on. Say which end drops, and tell the caller.
### 112 load shedding
id: load-shed
url: https://vibe-coding-101.brighteng.org/c/load-shed/
one_liner: Refuse the cheap requests so the expensive, important ones still finish. A polite 503 beats a timeout for everyone.
what: Load shedding drops or rejects work when the system is past a limit. Priority matters: health checks and paid checkout stay; search suggestions go. It is a choice you make in code, not something the kernel does kindly.
why_vibe: Models let every route compete until the database saturates and all of them fail. Name what you are willing to refuse first.
move: A concurrency or latency budget. Over budget, shed a named class of traffic with 503 and Retry-After. Do not shed the request that is mid-payment.
tell_the_model: When (service) is over budget, shed (low-priority routes) with 503. Keep (checkout or writes) running. Do not let every endpoint time out together.
pitfall: Shedding at random, including the health check. The balancer then kills the instance that was trying to survive.
### 113 dead-letter queue
id: dead-letter
url: https://vibe-coding-101.brighteng.org/c/dead-letter/
one_liner: Jobs that failed enough times go somewhere a human can see. They do not retry forever, and they do not vanish.
what: A dead-letter queue holds messages a consumer rejected past a retry limit. They wait for a fix and a replay. Without one, poison messages block a partition or get acked and lost.
why_vibe: Models catch errors, log them, and ack. The work is gone. Or they nack forever and the queue stalls. Name the retry cap and the dead letter.
move: Retry with backoff and a max. Then dead-letter with the error and the payload. Replay is an explicit action after the bug is fixed. Alert on depth, not on the first failure.
tell_the_model: Retry (job) with backoff up to (N) times. Then move it to a dead-letter queue with the error. Do not ack a failure away. Do not retry forever. Alert when the dead-letter depth is non-zero.
pitfall: A dead letter nobody owns. It is a landfill. Name who replays it.
### 114 liveness and readiness
id: liveness-readiness
url: https://vibe-coding-101.brighteng.org/c/liveness-readiness/
one_liner: Liveness: the process is not stuck, so restart it. Readiness: it can take traffic, so leave it out of the balancer until it can.
what: A liveness probe failure means kill and restart. A readiness probe failure means stop sending requests, but do not restart. Mixing them makes a database blip kill every pod, which then all restart and stampede back.
why_vibe: Models expose one `/health` that checks the database and wire it as liveness. The database hiccups, every instance restarts, and the hiccup becomes an outage. Split the probes.
move: Liveness is local: the event loop is scheduling. Readiness includes dependencies you refuse to serve without. Neither probe should be expensive or recursive.
tell_the_model: Split health checks. Liveness only proves the process is not deadlocked — do not check the database there. Readiness fails when (dependency) is down, and the load balancer stops routing. Do not restart on readiness failure.
pitfall: A readiness check that calls yourself through the public URL. The probe depends on the thing it is probing.
### 115 canary deploy
id: canary
url: https://vibe-coding-101.brighteng.org/c/canary/
one_liner: A small slice of traffic gets the new version first. If it burns, everyone else never sees it.
what: A canary sends a fraction of real traffic to the new build. You watch errors and latency. Then you raise the fraction or roll back. It is not a staging environment. Staging did not have this user’s data.
why_vibe: Models ship 100% because the pipeline only knows “deploy.” One bad release hits every tenant. Name the percent, the metric, and the automatic stop.
move: Start at a small percent. Compare error rate and latency to the old version. Abort on a threshold. Do not canary a database migration that old code cannot read.
tell_the_model: Canary (service) at (percent) first. Compare error rate and latency to the current version. Roll back automatically if the canary is worse. Do not send all traffic at once. Schema changes must be readable by both versions.
pitfall: A canary that shares a database migration the old code rejects. You canaried the app and broke the control group too.
### 116 single point of failure
id: spof
url: https://vibe-coding-101.brighteng.org/c/spof/
one_liner: The one box, file, or person whose death takes the system with it. Name it before you draw more boxes.
what: A single point of failure is a component with no standby. One database with no replica, one NAT, one certificate on one laptop, one queue with one consumer on one disk. Redundancy is a second thing that can take over, not a second arrow in a slide.
why_vibe: Models add a second app server and leave one database, one cache, and one availability zone. The diagram looks distributed. The failure domain did not change. Ask what dies alone.
move: List components that are count one. For each, say the standby or explicitly accept the outage. A replica you have never failed over to is not a standby yet.
tell_the_model: List single points of failure for (system). For each, add a real standby or write down that we accept the outage. Do not call it redundant unless failover has a path.
pitfall: Two instances in the same zone on the same disk. That is one failure with two processes.
### 117 failover
id: failover
url: https://vibe-coding-101.brighteng.org/c/failover/
one_liner: A named promotion when the primary dies. If you have never run it, you do not have it.
what: Failover moves work from a dead primary to a standby. Automatic failover is fast and can be wrong (split brain: two primaries). Manual failover is slower and safer. Both need a fence so the old primary cannot keep writing.
why_vibe: Models set `replica: true` and stop. Nobody has promoted it. Split brain is how you get two histories. Say who promotes, and how the old leader is fenced.
move: Write the steps: detect, fence, promote, repoint clients. Run them on a schedule against a staging pair. Prefer a short outage to two writers.
tell_the_model: Document failover for (store). Detect failure, fence the old primary so it cannot write, promote the standby, repoint clients. Do not allow two primaries. Do not claim automatic failover you have not run.
pitfall: Clients with the old primary’s address cached forever. Failover includes discovery, not only the database command.
### 118 pub/sub
id: pub-sub
url: https://vibe-coding-101.brighteng.org/c/pub-sub/
one_liner: Publishers do not know the subscribers. Each subscriber gets the event. A work queue is the opposite: one consumer does the job.
what: Publish/subscribe fans a message out to every interested subscriber. A queue hands a job to one worker. Mixing them up means either every worker sends the email, or nobody hears the event they needed.
why_vibe: Models use one “bus” for both. Two email workers both send. Or a cache invalidation only reaches one of five app nodes. Say fan-out versus competing consumers.
move: Events that notify: pub/sub, every instance subscribed. Jobs that must happen once: a queue. Do not share one topic for both without a rule.
tell_the_model: Use pub/sub for (event) so every instance receives it. Use a queue for (job) so exactly one worker runs it. Do not use one channel for both.
pitfall: Pub/sub with no retention and a subscriber that was restarting. It missed the event. If missing one is unacceptable, you needed a log or a queue, not a live fan-out.
### 119 tail latency
id: tail-latency
url: https://vibe-coding-101.brighteng.org/c/tail-latency/
one_liner: The average can look fine while the slowest 1% makes the page feel broken. Watch the tail.
what: Tail latency is the slow end of the distribution: p95, p99, not the mean. A request that waits on several services inherits the slowest one. Averages hide that. Timeouts and retries make the tail worse if you are careless.
why_vibe: Models log the average and ship. One in twenty checkouts takes 8 seconds. Name p99 and a timeout budget across the calls.
move: Measure p95 and p99 for the user-facing route. Give each downstream call a timeout that fits inside the parent budget. Do not retry a call that is already the tail.
tell_the_model: Track p95 and p99 for (route), not only the average. Each downstream call gets a timeout inside the parent budget. Do not retry so long that one slow call becomes three.
pitfall: Retrying every failure immediately. You turned a blip into a storm and lengthened the tail. Backoff and jitter, and a limit.
### 120 timeout budget
id: timeout-budget
url: https://vibe-coding-101.brighteng.org/c/timeout-budget/
one_liner: The user gave you one second. Every hop spends from that second. Do not give each hop its own one second.
what: A timeout budget is the time left for a request, passed down so callees stop before the caller has already given up. If the edge allows 800ms and the database is given 2s, the edge returns an error while the database keeps working.
why_vibe: Models set `timeout: 30` on every client. Stacked, a page can wait minutes, or the front times out and the back continues a write the user thinks failed. One budget, sliced.
move: Pick the user-facing limit. Subtract hops as you go. Cancel downstream work when the budget is gone. A write that might have committed needs an idempotency key, not a longer wait.
tell_the_model: One timeout budget for (request). Downstream timeouts must fit inside it. Cancel leftover work when time is up. Writes use an idempotency key so a retry is safe. Do not give every client 30s.
pitfall: A deadline that is checked once at the start and never again. The budget has to travel with the call.
### 121 graceful degradation
id: graceful-degradation
url: https://vibe-coding-101.brighteng.org/c/graceful-degradation/
one_liner: A dependency is down and the product still does the core job, minus the ornament.
what: Graceful degradation is a planned lesser mode: recommendations hidden, search fallback to a simple query, images optional. The alternative is a blank page because the ranking service timed out. It is designed, not an empty catch.
why_vibe: Models `try/catch` and return 500, or they swallow the error and show zeros. Name what the page still offers when each dependency is absent.
move: For each downstream call, write the fallback in the same change: hide the module, serve cached, or fail the request if the dependency is the product. Do not invent data.
tell_the_model: If (dependency) fails, (hide the widget / serve the last cache / fail the request — pick one). Do not 500 the whole page for an optional block. Do not invent fake results.
pitfall: A fallback that calls a second, heavier path. You degraded into a stampede. The fallback must be cheaper than the primary.
### 122 bulkhead
id: bulkhead
url: https://vibe-coding-101.brighteng.org/c/bulkhead/
one_liner: Separate pools so one noisy feature cannot sink the rest. Ships use them so one leak does not flood every compartment.
what: A bulkhead isolates resources: its own thread pool, connection pool, or queue. A slow export cannot borrow every database connection from checkout. Isolation is a limit, so the limited part fails first and alone.
why_vibe: Models share one HTTP client and one pool for everything. A partner API hangs and checkout cannot get a socket. Name the pool per dependency.
move: Give the risky or slow dependency its own small pool and timeout. When that pool is full, that feature fails. The rest keep their connections.
tell_the_model: Bulkhead (dependency) onto its own connection pool and timeout. If that pool is exhausted, fail (that feature) only. Do not let it take connections from (checkout).
pitfall: Ten bulkheads that together exceed the database’s max connections. You isolated the app and DDoSed the database. The sum of pools must fit under the server limit.
### 123 quorum
id: quorum
url: https://vibe-coding-101.brighteng.org/c/quorum/
one_liner: A majority must agree before a write is real. Two nodes out of three is enough. One node must not be.
what: A quorum is the minimum number of nodes that must confirm an operation. With 2f+1 nodes you tolerate f failures. A write quorum and a read quorum that overlap means a read sees the latest write. Split brain is what you get when two sides both think they have quorum.
why_vibe: Models set replicas to 2 and accept a write when either says yes. A partition then creates two truths. Say the numbers: N, and how many must ack.
move: Pick an odd replica count. Writes ack a majority. Reads, if they must be fresh, also hit a majority or a leader. Refuse to elect two leaders.
tell_the_model: Use a quorum for (store): N=(odd number). Acknowledge a write only after a majority. Do not accept a write from a single node if a partition could create a second writer.
pitfall: Majority of a changing membership you never recompute. A dead node still counts, so you can never form a quorum. Membership is part of the protocol.
### 124 backend for frontend
id: bff
url: https://vibe-coding-101.brighteng.org/c/bff/
one_liner: A thin API shaped for one client. The mobile app does not assemble five microservice calls on a flaky radio.
also: BFF
what: A BFF is a server owned by one client (web, iOS, Android) that calls internal services and returns the screen’s payload. It is allowed to be ugly and specific. Internal services stay client-agnostic.
why_vibe: Models either expose every internal service to the phone, or they build one “universal API” that satisfies nobody and over-fetches. Name the BFF when the screen and the services disagree.
move: One endpoint per screen or action, owned by that client. It fans out server-side. Do not put business rules that a second client will need only in the BFF — those stay downstream.
tell_the_model: Add a BFF endpoint for (screen) that calls (services) server-side and returns one payload. Do not make the client call internal services directly. Do not put shared domain rules only in the BFF.
pitfall: A BFF that becomes the only place pricing is computed. The next client reimplements it wrong. Shared rules live behind it.
## frontend
Hydration, SSR, hooks, keys, specificity, focus. Browser words. The model will ship a div with a click handler if you do not say them.
### 46 debounce
id: debounce
url: https://vibe-coding-101.brighteng.org/c/debounce/
one_liner: Wait until the action pauses, then run it. Search boxes, not every keystroke.
also: often paired with throttle
what: Debounce delays a function until events stop for N milliseconds. Throttle runs at most once per interval. Debounce is “after they finish.” Throttle is “while they are going.”
why_vibe: Without the word, you get “don’t spin while I type” and a request per letter. With it, one sentence. Cancel the in-flight request when the next one starts.
move: Search: debounce 300ms, abort the previous fetch. Scroll handlers: throttle. Do not debounce a submit button — that is a race dressed as UX.
tell_the_model: Debounce the search input by 300ms. Abort the in-flight request when a new one starts. Do not fetch on every keystroke.
pitfall: Debouncing the write path. Saves and payments want idempotency, not a delay.
### 47 optimistic UI
id: optimistic-ui
url: https://vibe-coding-101.brighteng.org/c/optimistic-ui/
one_liner: Paint the success first. If the server refuses, roll back. Do not make the user wait on a like.
what: Optimistic UI updates local state before the response. The rollback path is the feature. Without rollback you have a lying interface.
why_vibe: Models either wait on every click or fake the success with no undo. Name optimistic update and name the rollback in the same sentence.
move: Likes, stars, toggles: optimistic. Payments and deletes of other people’s data: wait. Always toast on rollback, not on success.
tell_the_model: Make (action) optimistic: flip UI on click, roll back and offer retry on failure. Do not wait for the response to paint. Do not lie if it failed.
pitfall: Optimistic create of a priced order. Money waits for the server.
### 55 SSR / SSG / SPA
id: ssr
url: https://vibe-coding-101.brighteng.org/c/ssr/
one_liner: Where the HTML is born: server on request, at build time, or only in the browser. Search, first paint, and auth all change.
what: SSR renders HTML on the server per request. SSG renders at build time. A SPA sends a shell and builds the page in JavaScript. Most apps mix them.
why_vibe: The model defaults to a client-only app. Marketing pages then show “enable JavaScript” to Google. Auth-only apps then flash a login on every load. Name the mode per route.
move: Public, searchable, cacheable: SSG or SSR. Logged-in app chrome: client is fine. Do not SSR a dashboard that needs cookies you did not pass.
tell_the_model: Marketing and article routes are SSG or SSR for search. The signed-in app may be client-rendered. Do not make the whole site a SPA by accident.
pitfall: Hydration errors ignored. If the server HTML and the client disagree, you do not have SSR. You have a flicker.
### 125 hydration
id: hydration
url: https://vibe-coding-101.brighteng.org/c/hydration/
one_liner: The server HTML is already there. Hydration attaches events without redrawing a different tree.
what: Hydration is the client taking server-rendered markup and binding behaviour to it. A mismatch means the client throws away the HTML and renders its own. The user sees a flash. The bug is that the two renders were not the same function of the same data.
why_vibe: Models turn on SSR and then read `window` or `Date.now()` during render. The trees differ. They “fix” it by rendering null until mount, which is client-only rendering with extra steps.
move: Same props on server and client. No clock, random, or locale side effects in render. If a bit must be client-only, mark that island, do not blank the page.
tell_the_model: Hydrate (page) without a mismatch. Do not read window, Date.now, or random during render. Server and client must emit the same markup from the same props. Client-only widgets are islands, not `if (mounted) return null` around the whole page.
pitfall: Suppressing the hydration warning. The warning was the bug report.
### 126 code splitting
id: code-splitting
url: https://vibe-coding-101.brighteng.org/c/code-splitting/
one_liner: The first screen does not download the admin editor. Load a route when the user opens it.
what: Code splitting breaks the bundle into pieces loaded on demand. The entry chunk is what the first paint needs. The rest waits for a route, a tab, or an interaction. A split that loads immediately is not a split.
why_vibe: Models import the chart library into the homepage “so it is ready.” The phone downloads a megabyte before the title. Name the entry chunk and what is lazy.
move: Lazy-load routes and heavy widgets. Keep the shared shell small. Do not prefetch everything on load. Show a local placeholder, not a blank app.
tell_the_model: Code-split (feature) out of the entry bundle. Load it when the route opens. Do not import it from the root layout. The first paint must not wait on it.
pitfall: Splitting into a hundred files that all load at once. You paid the request cost and kept the weight.
### 127 tree shaking
id: tree-shaking
url: https://vibe-coding-101.brighteng.org/c/tree-shaking/
one_liner: Dead exports are dropped from the bundle. A side-effect import keeps the whole module.
what: Tree shaking removes unused exports when the bundler can see that they are pure. `import _ from "lodash"` drags the library. `import { debounce } from "lodash-es"` can leave the rest behind. A module that runs code on import is not shakeable.
why_vibe: Models install a utility kitchen sink and import the default. The bundle grows and they blame the framework. Name the import style.
move: Import the function you call. Avoid import-for-side-effect except polyfills you mean. Check the bundle for libraries you only used once.
tell_the_model: Import only (function) from (package). Do not import the default barrel if it pulls the whole library. Do not add a side-effect import “just in case.”
pitfall: A barrel file that re-exports everything and is imported by the app root. Shaking stops at that barrel.
### 128 controlled input
id: controlled-input
url: https://vibe-coding-101.brighteng.org/c/controlled-input/
one_liner: React state owns the value, or the DOM does. Mixing them makes the cursor jump and the submit lie.
what: A controlled input’s value comes from state on every render. An uncontrolled input keeps the value in the DOM until you read it. Switching a component from one to the other, or setting `value` without `onChange`, freezes the field.
why_vibe: Models set `value={state}` and also `defaultValue`, or they forget `onChange`. The field looks disabled. Name which owner you want and stick to it.
move: Forms that validate as you type are controlled. Simple native submits can be uncontrolled. Do not do both on one node. Reset by changing a key or clearing state, not by fighting the DOM.
tell_the_model: Make (field) a controlled input: value from state, onChange updates state. Do not also set defaultValue. Do not set value without onChange.
pitfall: Controlling a field from a server round-trip on every key. The cursor jumps and you invented a race. Local state, submit later.
### 129 reflow and repaint
id: reflow
url: https://vibe-coding-101.brighteng.org/c/reflow/
one_liner: Reading layout in a loop forces the browser to recalculate. Write styles, then read once.
what: Reflow is recomputing geometry. Repaint is recomputing pixels. Interleaving reads (`offsetHeight`) and writes (`style.width`) in a loop thrashes layout. Transforms and opacity can stay on the compositor. Top, left, and width often cannot.
why_vibe: Models animate `left` and measure height inside a scroll handler. The main thread drops frames. Name the thrash or they will add more listeners.
move: Batch reads, then batch writes. Animate transform and opacity. Do not bind scroll to layout reads without a frame budget. Virtualize long lists instead of measuring every row every time.
tell_the_model: Do not interleave layout reads and writes. Animate transform/opacity, not top/left/width. Do not read offsetHeight inside the loop that sets sizes.
pitfall: “Optimizing” by caching a measurement that is stale after a font load. Measure after layout, once.
### 130 progressive enhancement
id: progressive-enhancement
url: https://vibe-coding-101.brighteng.org/c/progressive-enhancement/
one_liner: The basic action works as HTML. Script makes it better. Script must not be the only way.
what: Progressive enhancement starts with working markup: a form that posts, a link that navigates. JavaScript upgrades that. The opposite is a button that does nothing until a bundle arrives.
why_vibe: Models build the happy path entirely in an onClick. No URL, no submit, no failure if the script 404s. Name the baseline.
move: Real `