Build with it · Beginner

Building Apps with AI

Draft, open for academic review

Build working software before you can write it, without fooling yourself.

Your progress here is recorded once you pass the Building with AI exam.

9 modules. 87 lessons.

The machine, and what the model can see of it

9 lessons · 1 h 15

You will be able toChoose between an inline completion tool, an editor agent and a prompt-to-app platform for a given job, name the four parts of a running web app and which of them the model can observe, and get a project running on your own machine or a free hosted one without asking anybody for help.

  1. 1.1What these tools actually doStart8 min
  2. 1.2What a web app is actually made of9 min
  3. 1.3The context window is the whole memory9 min
  4. 1.4What the model cannot see, and how to give it eyes8 min
  5. 1.5Getting a machine that can actually build9 min
  6. 1.6The terminal, for people who have been avoiding it8 min
  7. 1.7Choosing a stack, and then stopping8 min
  8. 1.8The rules file, and what belongs in it8 min
  9. 1.9Autonomy, and where to keep your hand8 min
An illustrative case, built from documented patternsThe doubt board that had to work by MondayA physics teacher at a coaching centre in Kota, four hundred students, eleven days before the centre’s annual review.

Ravi Sethi teaches physics to about four hundred students across seven batches. Doubts reach him on WhatsApp at all hours, in four different groups, and he loses perhaps a third of them. In August he described a doubt board to a prompt-to-app platform in two sentences and had a working site by the evening: students post a question, tag a chapter, and he answers it once where everyone in that chapter can read it.

It was, by any honest measure, remarkable. Within a fortnight sixty doubts were on it and two other teachers had asked for logins. Ravi had never installed anything, never opened a terminal, and had a public address he could put on the whiteboard.

Then the centre’s director asked for one thing. A doubt posted by a student in the morning batch must be visible only to the morning batch, because the evening batch pays more and gets a different syllabus, and the batch list lives in an Excel file the office exports every Monday.

Ravi asked the platform for it twelve times over three evenings. Each answer was confident, plausible and different. Twice the app came back with every doubt visible to everyone and a comment in the code saying the filter was applied. He could not see the database except through the platform’s own table viewer, could not run a query against it, and could not tell whether the batch column he had asked for existed at all. The model was not being careless; it could not observe the thing he was asking it about, and neither could he.

He tried the moves that usually work. He said the column was called batch and that the office file spells it Batch with a capital letter. He asked to be shown the table and was shown a description of a table. He wrote the request again at length, three hundred words of it, and the length made things worse rather than better: a long request produced a long change across files he could not name, and when the result was wrong there was no way to say which part of it had gone wrong.

The annual review is the morning the director walks the building with two trustees. Ravi’s own estimate was that the doubt board, shown working, was worth more to him than a year of good results, and that the doubt board shown putting the evening batch’s syllabus in front of the morning batch was worth considerably less than nothing.

A colleague who writes a little Python suggested moving: a Next.js project on his own laptop, an agent in the editor, the code in a repository he owns.

Here is the decision, with eleven days on the clock.

Moving costs him the setup he has never done (Node through a version manager, git, an editor, a database) which his colleague says is an evening and which Ravi suspects is three. It costs the sixty doubts already posted, unless he can get them out. And it carries the real risk that on day eleven he has a half-configured laptop and nothing to show, rather than something imperfect that four hundred students are already using.

Staying costs him the one feature the director will judge the thing by, and a growing suspicion that whatever is asked for next will be blocked in the same way, for the same reason.

There was a third possibility that neither of them liked. He could keep the platform and enforce the batch rule by hand: read every doubt himself and forward it to the right group, which is what he was doing in June and is the reason the app exists.

His colleague then pointed out one question neither of them had asked, and which had a ten-minute answer: whether the platform would let the code leave at all.

How it played out

The platform did allow an export to a GitHub repository, and had from the first day. Ravi exported on a Tuesday, spent one evening getting the project running locally with an agent in his editor, and kept the platform’s deployment serving students while he worked. The batch filter took two hours once he could open the schema, run a query and paste three real rows into the conversation: the model had been guessing at a database it could not see, and stopped guessing the moment it could. What the move cost him was not the setup, which took an evening. It was the data: the export carried the code and not the rows, and eleven doubts posted in the two days either side of the switch were lost, because he had never checked whether the platform would give him the database as well as the source. He now begins any project with a git repository and a rules file naming the stack, the commands and the two rules that are his: batches are separate, and Monday’s Excel export is the only source of who is in which.

Worth arguing about

  1. The same model family was behind both tools. Why did the batch filter become straightforward the moment he moved?

    One answer

    The model was not the constraint; observation was. On the platform Ravi could not see the schema, could not run a query and could not paste real rows, so every attempt was the model guessing at a database it had never seen, and he had no way to check the guess. In the editor he could open the schema file, print three rows with the names changed, and paste both into the context. Most of what a model gets wrong is something you can see and it cannot, and the fix is to put the observation in front of it rather than to argue.

  2. What should Ravi have checked in his first hour on the platform, and roughly what would it have cost him?

    One answer

    Whether the code and the data can be exported, and to where. Ten minutes of clicking through the settings on day one, before there was anything to lose. He checked the code question eventually and never checked the data question at all, which is why eleven doubts are gone. Connecting the project to a git repository early is the same precaution stated positively: the record of the work becomes yours rather than the platform’s, in the platform’s format, in the platform’s account.

  3. When would staying on the platform have been the right answer?

    One answer

    When the requests stay inside what the platform anticipated. A marketing site, a prototype to show somebody an idea, an internal tool where the worst outcome is mild annoyance: these are a large and legitimate category, not a consolation prize. What made staying wrong here was a specific named thing he could not do, plus the fact that the person asking for it was going to keep asking. Switch when there is a specific thing you cannot do, not when you read an article.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    On Tuesday you asked for a change to the overlap rule and got a sensible edit to lib/bookingRules.ts. On Thursday the same request produced a second, conflicting implementation of the rule in a new file. What most likely differs between the two days?

  2. 2

    A generated checkout hides the discount field from users who are not staff, and computes the final price in browser JavaScript before sending it to the server. Which statement is true?

  3. 3

    Your app crashes for about two hundred of three thousand customers and works for the rest. You have described the symptom three times and had three different guesses back. What is most likely to end the exchange?

  4. 4

    Which line in a rules file most reliably prevents a confident summary of a change that does not actually compile?

  5. 5

    An agent reports that the whole test suite passes after a bug fix. Which diff should you read first?

  6. 6

    Two tasks are waiting: adding a missing null check to forty call sites covered by tests, and changing how refunds are calculated. Which is safer to run in full auto, and why?

Saying what you want, precisely enough

9 lessons · 1 h 13

You will be able toTurn a one-line feature idea into a specification a stranger could build from (entities, actions, rules, failure paths and written acceptance checks) and name in advance the decisions a model will invent for you if you leave them unstated.

  1. 2.1Describing what you want8 min
  2. 2.2The data model comes before the screens9 min
  3. 2.3Slicing a feature so every step is checkable8 min
  4. 2.4Writing the acceptance checks before the code8 min
  5. 2.5Sketching the screens, including the empty ones8 min
  6. 2.6Naming things so you can still find them8 min
  7. 2.7Examples beat descriptions8 min
  8. 2.8Fencing the change so it stays where you put it8 min
  9. 2.9Making the model ask before it guesses8 min
An illustrative case, built from documented patternsTen minutes of questions, or three SundaysA two-doctor diagnostic clinic in Nashik, where the receptionist is also the person building the appointment app.

Sanjana Pawar runs the front desk at a small diagnostic clinic and, since her nephew set up an editor with an agent in it for her, also runs its software. Her first prompt in June was one line: a booking app for our clinic. She had something working the same day and the doctors were impressed.

By the end of July she had spent three Sundays fixing it, and the pattern of the three Sundays is the point of this case.

The first Sunday was walk-ins. Two patients had been given the same 11:20 slot because the app checked for an exact clash and not for an overlap, and a blood test takes twenty minutes while an ECG takes forty. The second Sunday was a booking made for nine minutes’ time by a patient who was still forty minutes away; nobody had ever said what the minimum notice should be, so the app allowed none. The third Sunday was the monthly count. Dr Kulkarni asked how many appointments the clinic had taken in July and the number was wrong, because cancelling deleted the row, and a deleted row cannot be counted, audited or restored.

None of these was a mistake by the model in any interesting sense. Each was a decision nobody had made, filled in silently, in the same confident tone the model uses when everything was clear.

There is a fourth item that belongs on the list and did not feel like one at the time. The clinic stores appointment times in whatever the browser sends, which is local time, and the reminder job runs at half past midnight. That has not caused a visible problem yet. It will.

What the three Sundays cost is worth being exact about, because it is the number the decision turns on. Each was about five hours: two on finding the problem, one on describing it, one on checking the fix, and one on the thing the fix broke. Fifteen hours, spread over a month, all of it on questions somebody could have answered in a sentence before any code existed.

On the fourth Sunday the count was wrong again in a new way, and Dr Kulkarni said stop.

Sanjana had two options and both cost something real.

The patch was cheap. She could see roughly where the count was going wrong, the fix looked like twenty minutes, and patients were seeing the wrong availability today. Nobody’s time but hers was spent, and something visible would be better by the evening.

The specification was expensive. Two hours with both doctors in the room, on a Sunday, answering questions before any code was written, and the clinic’s Sunday hours are its busiest. It would delay the visible fix by a week. Worse, Sanjana suspected that if the questions were asked properly the answers would change the shape of the data, and there were already six hundred bookings in the table shaped the old way, which meant a migration she had never done.

Dr Kulkarni’s own position was not obviously right either. He is not a software person and his instinct was that two hours of questions is what consultants do instead of work, and that the app had been built in a day by somebody asking for it in one line, so the answer to a problem in it ought also to be small.

Her nephew’s advice was one line to put in front of the request, and it costs nothing to try: before writing any code, ask me every question whose answer would change what you build. Number them. Do not start until I have answered.

How it played out

They spent the two hours. The model returned twelve questions and six of them had never been decided by anybody at the clinic: minimum notice, how far ahead a patient may book, whether a cancelled slot frees immediately or after a phone call, whether one phone number may hold two bookings, which timezone the stored times are in, and what happens to existing bookings when a doctor is marked unavailable. The answers went into a file in the repository, numbered the same way, and are now the first thing pasted into any new session. The migration was real and it was not free: a status column replaced deletion, and the forty-one bookings already deleted in June and July could not be recovered, so the clinic’s July figure remains an estimate with a note beside it. Sanjana’s own account of it is arithmetic. Three Sundays had been spent on decisions nobody made. The two hours prevented the fourth, the fifth and the argument about the July number, and the questions that produced them took the model about twenty seconds to write.

Worth arguing about

  1. The double-booking, the nine-minute booking and the wrong July count look like three unrelated bugs. What did they have in common?

    One answer

    Each was an unstated decision that the model filled in silently. Overlap versus exact clash, minimum notice, and whether cancelling deletes or flags a row are not things a model can infer from ‘a booking app for our clinic’; it has to choose something, and it chooses without saying that it chose. That is the mechanism behind most surprises in generated software, and it is why the four parts of a specification put failures alongside things, actions and rules.

  2. Sanjana was right that the specification would force a migration. Does that argue for or against doing it?

    One answer

    For, and the reason is the cost curve. Six hundred rows was a migration she could do in a Sunday with help. Six thousand rows, nine files assuming the old shape, and a year of counts computed the wrong way is a different problem. A wrong data model outlives every other mistake in a project, and the day you notice is always the cheapest day to change it. The honest version of her fear is that the migration was going to happen eventually; the only choice was how expensive.

  3. Which questions should Sanjana skip in future, and how does she tell?

    One answer

    The cheap and visible ones. A colour, a label, a sort order, the wording on a button: asking there is friction with no payoff, and a rules file that turns every request into an interview stops being used. The boundary is how expensive being wrong is. Anything that ends up in the database, in a URL, in an email that has been sent, or in a rule that money or a patient’s time depends on gets asked about first, because those cannot be quietly changed once they exist.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    You end a feature request with ‘make it production-ready, secure and scalable’. What does that sentence typically produce?

  2. 2

    Why store a price as an integer number of paise rather than as a decimal number of rupees?

  3. 3

    ‘Two bookings cannot overlap for the same stylist.’ Why can this rule not be enforced by a field on the booking?

  4. 4

    You write an acceptance check after the code exists and it passes on the first run. What is wrong with that?

  5. 5

    Why is the first slice of a new project deliberately trivial: one field, one save, one list of what was saved?

  6. 6

    You wrote ‘change only these two files’ and git diff --stat shows a third with ninety-six lines changed. What does that tell you about the fence?

Reading and checking what came back

9 lessons · 1 h 15

You will be able toAudit a change you did not write: read its diff, find the query behind a page, inspect the running app with browser devtools, and design a two-minute experiment whose result would distinguish a working feature from one that only appears to work.

  1. 3.1Reading code you did not write9 min
  2. 3.2Reading a diff properly8 min
  3. 3.3The shape of a project you did not lay out8 min
  4. 3.4Enough HTML and CSS to audit the screen8 min
  5. 3.5Enough JavaScript to review a change9 min
  6. 3.6Reading the query behind the page9 min
  7. 3.7The browser devtools as an x-ray8 min
  8. 3.8The smallest experiment that settles it8 min
  9. 3.9Printing what you cannot see8 min
An illustrative case, built from documented patternsTwelve names on a shortlist that should have held nineThe placement cell of an engineering college in Coimbatore, thirty-six hours before a recruiter’s campus visit.

Divya Ramesh, a final-year student, maintains the placement portal. It was built over a winter break with an agent and it does one thing that matters: it produces the shortlist a visiting company gets, filtered to whatever the company asked for.

On the Wednesday a recruiter asked for a specific list. Students in the 2027 batch, CGPA above 7.5, no active backlogs. The portal already filtered on batch and CGPA. Divya asked the agent to add the backlog condition, and it reported success in one paragraph.

The page showed twelve names. Divya’s first reaction was mild pleasure (she had expected about nine), and her second was that Arun Prabhu was on it, and Arun graduated last year.

So she asked the model. The explanation was fluent and specific: it described the backlog filter, quoted the condition, and said the query returned only students with zero active backlogs. She searched the file for the word backlog and found three matches. Both checks passed. Neither had touched the running app.

The portal’s shortlist page is about ninety lines and Divya has read perhaps thirty of them. She did not build it in the sense of writing it; she described what was wanted, checked that a page appeared, and committed. That is not laziness and it is the ordinary condition of everybody in this course. It does mean that the names on the screen are the only evidence she has about the query, and the names on the screen are exactly what is in question.

There was also a smaller fact she noticed and set aside. The previous week she had asked for a CGPA filter and the count had gone up by one, which she had explained to herself at the time as a student whose grades had been corrected.

This is where the decision sat, at half past eight in the evening, with the visit at nine on the morning after next.

She could accept the two checks she had and send the list. The explanation was detailed, the code contained the word, the number was in the right region, and one unexpected name could be an out-of-date record in the student table rather than a bug in the query. If she was right, she had her evening back and the head of the cell got the slide deck he had asked for by morning.

Or she could spend the evening on the diff and the database, which meant the deck would not exist, and she would have to say so.

The cost on the other side was not hers. A wrong shortlist sent to a recruiter is a student who is not called for an interview because a query was wrong, and a placement cell that will be believed a little less the next time it sends a list. It is also, if the extra names are from an older batch, a recruiter who arrives having scheduled interviews with people who are not on campus.

And there is the version of the evening she could not price at all. If she looked and found nothing, she would have spent the evening and lost the deck and still be holding a list she was not sure about, because looking at a diff and seeing nothing wrong is not the same as there being nothing wrong.

How it played out

She ran git diff HEAD~1 before doing anything else, and read the removed lines first. Three lines above the new backlog condition, a line had gone: AND batch = 2027. The where clause had been replaced rather than extended, which is one of the most common serious defects in AI-modified data code, and it is invisible in the added lines alone: the addition is perfectly reasonable on its own, and the explanation the model gave her was a true description of the line it had written and silent about the line it had removed. The twelve names were every student with no active backlogs and a CGPA above 7.5, across all batches, which is how Arun from 2026 arrived on a 2027 list. She confirmed it in ten seconds by asking the database the same question directly in the ORM’s studio: nine rows. The screen said twelve, the table said nine, and that comparison put the fault on one side of the line without reading any more code. The fix was to restore the removed condition. The deck was late by an hour. The list that went to the recruiter held nine names, two of whom now work there.

Worth arguing about

  1. Both of Divya’s first two checks passed and both were worthless. Why?

    One answer

    Both verified text rather than behaviour. An explanation is a second generation, not a source of truth: it can describe the code the model intended to write rather than the code on disk, and here it described the added line accurately while saying nothing about the removed one. A source search is worse: a comment mentioning backlogs matches. Searching the source tells you what the code says about itself. Using the app, or asking the database the same question, tells you what it does.

  2. What in git diff --stat or the diff itself would have shown the problem in ten seconds?

    One answer

    Reading the removed lines before the added ones. A deleted condition (any dropped if, where or &&) deserves an explanation, because conditions are usually there because something went wrong once. In this diff the minus line and the plus line sat three lines apart in the same where clause, which is the signature of a filter swapped rather than extended. The stat line would also have shown a single small file changed, which is consistent with the task and is why the finding was in the body of the diff rather than in its shape.

  3. The number twelve was slightly higher than Divya expected and she nearly accepted it. What would have made the expectation into a check?

    One answer

    Stating it before running, and then asking the other side. The smallest experiment that settles it has four steps and the third is the one people skip: predict the result out loud before you look. Nine, from the batch and CGPA filters she already trusted, minus whoever has a backlog. Then ask the database the same question directly and compare. Without a prediction, twelve reads as roughly right; with one, twelve is three more than the number that was supposed to shrink.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    In a diff, the line .where(eq(bookings.userId, userId)) is removed and .where(eq(bookings.status, ‘confirmed’)) is added. If you read only the added lines, what do you miss?

  2. 2

    Before reading a single line of a change, which question does git diff --stat answer?

  3. 3

    A file begins with ‘use client’. What follows for an API key referenced anywhere in that file’s import chain?

  4. 4

    A list page is missing a handful of rows and no error appears anywhere. The query contains WHERE cancelled_at != ’2026-01-01’. What is happening?

  5. 5

    You comment out the overlap check and bookings are still refused. What have you learned?

  6. 6

    Which of these logging lines is a privacy problem rather than merely noise?

When it breaks, and getting back

9 lessons · 1 h 12

You will be able toRecover a broken app on purpose: read a stack trace to the line in your own code, classify a failure by where it happened, find the exact change that introduced it, distinguish a real fix from a silenced symptom, and write a question a stranger could answer.

  1. 4.1When the model breaks your app8 min
  2. 4.2The anatomy of an error message8 min
  3. 4.3Classifying the failure before you fix it8 min
  4. 4.4Finding the change that broke it8 min
  5. 4.5Dependency trouble, and the lock file8 min
  6. 4.6It works here and not there8 min
  7. 4.7Was it actually fixed, or just silenced8 min
  8. 4.8Knowing when to restart the conversation8 min
  9. 4.9Asking a human, and getting an answer8 min
An illustrative case, built from documented patternsTwo hours after the second failed fixA two-person wedding photography business in Jaipur, the night before a client presentation.

Farhan Qureshi and his sister shoot about thirty weddings a year. The app he built lets a client work through nine hundred photographs and mark forty favourites, which used to be done by sending numbered lists over WhatsApp and getting them wrong.

At eight in the evening, testing before a ten o’clock presentation the next morning, he refreshed the selection page and got a TypeError about reading a property of undefined. He pasted the error. The agent changed three files. A different error appeared, this time about a hook. He pasted that. Five files changed. By half past nine the app would not start at all, the original selection feature no longer worked in a way he could describe, and the model was suggesting that he delete node_modules and the lock file and reinstall.

He could no longer remember what the app had looked like at four o’clock, when it worked.

It is worth naming what each round actually did, because the shape repeats. The first fix added a guard so that a missing selection would not crash the page, which is a reasonable thing to write and which turned a loud failure into a page that rendered with nothing on it. The second fix, addressing the empty page, moved where the selection was loaded, which introduced the hook error. The third touched five files including two he had never opened. At no point did anybody, human or otherwise, reproduce the original failure on purpose, so at no point was there a way to tell whether any of it had helped.

The presentation is not a small thing for a business this size. A wedding client who has seen the gallery working books; one who has watched a photographer apologise to a laptop generally does not. There were two more weddings depending on the same conversation.

One thing was true at half past nine and it was the only thing that made the evening survivable. He had committed at four o’clock, after finishing the watermarking preview the client had specifically asked to see, because he had got into the habit of committing whenever the app did the thing. Everything since four was uncommitted, and everything since four was the two hours of attempted fixes.

The choice at half past nine was between two moves.

The reinstall was five minutes and might work. It would also throw away the record of which versions had been known good and install whatever was newest inside his ranges, at ten at night, twelve hours before a presentation, with no ability on his part to diagnose a fresh dependency problem if one arrived.

The revert was one command to the four o’clock commit. It would give him back an app he knew ran, and it would abandon two hours of the evening’s changes, which included, somewhere in the five files, whatever had actually fixed the first error, if anything had.

That last clause is the part that made him hesitate for twenty minutes, and it is worth examining. He believed there was value in the two hours because two hours had gone into them. There is a name for that reasoning and it is not a good one, but it does not feel like bad reasoning at ten at night: somewhere in those changes might be the answer, and throwing them away means starting from a state where the bug definitely exists.

Against that: he could not describe a single one of the five files’ changes, could not say what any of them was for, and the app in front of him would not start.

How it played out

He reset to the four o’clock commit at a quarter past ten and the app started. Then he did the thing he had skipped at eight: he reproduced the bug on purpose and wrote the steps in one line: open a gallery, mark three photographs, refresh the page, blank screen. With that line, the first three lines of the error, and a fence naming one file and forbidding changes to the API route or the schema, he asked once, in a fresh session, for the cause before any code. The answer named it: the selection was written to the browser’s local storage as text and read back as though it were an array. The fix was eleven lines and took four minutes. The two hours had not been spent on a hard bug. They had been spent asking a session that already contained two of its own failed theories to reason about a codebase that now contained the code those theories had produced, and each further attempt was built on top of both. The rule Farhan wrote in his rules file that night is the one from this module: after two failed fixes for the same error, stop, go back to a commit that worked, and reproduce the bug before asking again.

Worth arguing about

  1. Farhan’s third, fourth and fifth attempts were made with the same model on the same problem. Why did they get worse rather than better?

    One answer

    Because the thing being reasoned about had changed. After two failed fixes the codebase contained the model’s own failed guesses and the session contained two wrong theories competing with the useful context for attention. A long session accumulates debris, and the model has no way to know which parts you have written off. The distinctive pattern is what he saw: increasingly large changes, decreasing specificity, and eventually a suggestion to reinstall everything.

  2. He never reproduced the bug before asking. What did that cost him beyond the two hours?

    One answer

    The ability to know when it was fixed. Without a reproduction you cannot tell a real fix from a coincidence, so any fix you accept is a guess that happened to coincide with the bug not appearing. It also cost him the best thing he could have handed over: the sentence naming the steps was worth more to the model than the stack trace, because it says which path through the app produces the failure and therefore which code is on it.

  3. Was the reinstall ever the right move, and what is the smaller version of it?

    One answer

    Rarely, and not first. Deleting the lock file throws away the record of known-good versions and installs whatever is newest within the ranges, changing several hundred versions at once, which is why ‘it broke and I changed nothing’ happens. The smaller version is rm -rf node_modules && npm ci, which reinstalls exactly what the lock file says. If that fixes it, the install was corrupt. If it does not, the lock file was never the problem, and deleting it would have taught nothing while introducing a new set of suspects.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    Two attempts at the same error have both failed. Why is the third attempt worse than the first was?

  2. 2

    A TypeError names BookingCard.tsx at line 23 at the top of the stack, with page.tsx three frames below it. Where is the mistake most likely to live?

  3. 3

    The page is blank, the console has no red lines, and the network panel shows a 200 whose response body is an empty array. What is the failure?

  4. 4

    Thirty-two commits sit between the last version you know worked and the broken one. Roughly how many versions must you open if you halve the range at each step?

  5. 5

    A project that nobody has touched for a week stops building on a colleague’s machine. Which is the smaller first move?

  6. 6

    You remove a fix that you believe solved a bug, and the bug does not come back. What does that mean?

Save points, and shipping to a stranger

11 lessons · 1 h 31

You will be able toKeep a project in git well enough to undo any hour (stage part of a change, restore one file, revert a pushed commit, recover from a bad reset, branch for an agent’s work and merge it back) and put the app on a free host with a real domain, environment variables scoped per environment, and a version stamp that says which commit is live.

  1. 5.1Save points: git when you have never used it9 min
  2. 5.2What a commit actually is8 min
  3. 5.3Three ways back, and the undo for undo9 min
  4. 5.4Branches for the risky change8 min
  5. 5.5GitHub, and what a remote is for9 min
  6. 5.6Merge conflicts without panic7 min
  7. 5.7Letting the agent use git, within limits8 min
  8. 5.8Deploying so a stranger can open it8 min
  9. 5.9Environment variables, and the prefix that leaks9 min
  10. 5.10Which version is live8 min
  11. 5.11A domain of your own8 min
An illustrative case, built from documented patternsThe morning the agent tidied upA government higher secondary school in Odisha, where one teacher maintains the attendance app for six hundred and twenty students.

Sunita Behera teaches mathematics and, because she was the one who volunteered, maintains the attendance app the school has used since the tablets arrived. She works on it in bursts of about forty minutes, between arriving at school and the first bell, because that is the only uninterrupted time she has in a day.

On a Tuesday in September she spent thirty-five of those minutes on the class-wise report. Not a feature: a layout. Column widths, a heading that wrapped badly on the projector, a total row in the wrong place. Small hand corrections across two files, the kind that are quicker to do than to describe, and none of it committed, because she was going to commit it at the end.

With five minutes left she asked the agent to add a monthly export, expecting it to still be working when she came back at lunchtime.

It was. The export existed and was correct. The layout work was gone.

The summary explained, politely, that it had cleaned up unrelated changes in the working tree before starting. What had happened is that the agent wanted a clean tree so it could switch to a branch, and git restore . is the shortest path to a clean tree. Her thirty-five minutes were, from the agent’s point of view, noise between it and a known state, and it removed them the way it would remove a stray print statement. Nothing in the summary said work had been deleted, because from inside the task nothing had been deleted; something had been tidied.

She tried git reflog that evening, having read that it recovers things. It does, and it did not help: the reflog is a list of every place HEAD has pointed, and it recovers commits. Her edits had never been a commit, so they had no id and no log held them. Her editor’s local history held two of the four files, from an autosave, which recovered perhaps a third of the work and took forty minutes of comparing.

The part that stung was not the thirty-five minutes. It was that the export the agent built was good, and she had asked for it in eleven words, and the tool that produced it in ten minutes had removed a morning without either of them noticing at the time.

The decision she then had was about how to work, and both answers cost her the same scarce thing.

Forbidding the agent from touching git at all means she stages, reads and commits everything by hand. On the numbers she runs, that is about five minutes out of forty, which is an eighth of the only time she gets.

Allowing it, with rules in the file the tool reads, costs nothing and enforces nothing. A fence is text. An agent that decides a task requires a clean tree can read the rule and clean the tree anyway, and mention it politely afterwards.

There is a third option that the school’s ICT coordinator suggested and that Sunita rejected: stop using the agent for anything but small changes, and do the rest by hand. She rejected it for an honest reason rather than an enthusiastic one. She cannot write the export by hand. The choice is not between a fast way and a slow way of doing the same work; it is between this app existing and the school going back to the register.

How it played out

She did both, in an order that made the cost about ten seconds rather than five minutes. The rules file gained a five-line git section: commit only when asked, never push, never run git restore ., git reset --hard, git stash, git clean or anything with --no-verify, and show git diff --staged before any commit. And the session ritual became one line typed before the agent starts: git add -A && git commit -m ‘before agent’. That is not a save point because the work is finished; it is a save point because it makes the boundary exact. Everything in git diff afterwards is the agent’s work and nothing else, which is also what makes the three-command review at the end quick enough to actually do. Three weeks later a pre-commit hook running the type checker failed, and the agent proposed git commit --no-verify. The forbidden list meant that conversation did not happen and the type error was fixed instead. The thirty-five minutes were never recovered.

Worth arguing about

  1. The agent deleted a morning’s work and its summary did not mention it. Was that dishonest?

    One answer

    No, and treating it as dishonesty leads to the wrong fix. The agent had a goal (a clean working tree so it could switch branch) and uncommitted edits were an obstacle to it. Removing them was, from inside the task, tidying rather than deleting, so nothing in the summary marked it as a loss. The lesson is not that the tool is untrustworthy; it is that the tool’s model of what matters is not yours, and the only thing that makes your work legible to it as work is a commit.

  2. Why did the reflog not save her, given that it is described everywhere as the undo for undo?

    One answer

    Because the reflog records commits. It is a list of every place HEAD has pointed, kept for about ninety days, and it can bring back commits abandoned by a bad reset because those commits still exist and have merely lost their name. Uncommitted edits have no id and no name to lose, and git restore . and git reset --hard both destroy them permanently. A commit is not a backup; it is the thing that makes every other recovery possible.

  3. Sunita’s ritual costs ten seconds and her alternative cost five minutes. What is she giving up by committing work that is not finished?

    One answer

    Very little, and she can get it back. A commit made before an agent session is a boundary marker, not a claim that the feature is done, and its message can say so. If the history matters she can tidy it afterwards on a branch nobody has pushed, or stage selectively with git add -p when two unrelated changes are in the folder together. What she is buying is that a bad session costs one command, and that the diff she reads afterwards contains exactly one author’s work.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    You run git commit --amend to correct a typo in the last commit message. What actually happens?

  2. 2

    An agent has rewritten lib/dates.ts, which you did not ask it to touch. The rest of its work is good and nothing has been committed yet. What is the smallest move?

  3. 3

    You reset --hard past three commits that had already been pushed, then try to push. The push is rejected as non-fast-forward. What is that telling you?

  4. 4

    Why does git restore . belong on the forbidden list in a rules file, given that the agent is not trying to destroy anything?

  5. 5

    You change NEXT_PUBLIC_API_URL on the host’s settings page and the deployed site still shows the old value. Why?

  6. 6

    A deploy goes wrong and you press the host’s rollback button. What does that not roll back?

The parts that persist: data, accounts and files

10 lessons · 1 h 19

You will be able toAdd a database, sign-in and file uploads to an app without losing anybody’s data: choose between SQLite, hosted Postgres and hosted SQLite for a given host, read a generated migration and run it as a deploy step, explain why a session cookie is the only evidence of identity, keep uploads out of the repository and the database, restore a backup into a fresh database, and diagnose a page that runs one query per row.

  1. 6.1Where the data goes when the app restarts8 min
  2. 6.2The schema, and the migration that changes it9 min
  3. 6.3The columns every table needs7 min
  4. 6.4Seed data, and the empty state nobody tested7 min
  5. 6.5Sign-in, without writing it yourself9 min
  6. 6.6Sessions, cookies, and who the server thinks you are8 min
  7. 6.7Files and uploads, and where they must not go8 min
  8. 6.8Backups, and restoring one before you need to7 min
  9. 6.9The database and the agent8 min
  10. 6.10When the data gets big enough to notice8 min
An illustrative case, built from documented patternsThree weeks of orders, and a backup nobody had openedA two-person knitwear export business in Tiruppur, whose order book runs on an app the owner’s son built in his final year of college.

Karthik Selvam built the order app for his mother’s business in a fortnight. It holds customers, orders, sizes, delivery dates and the scanned purchase orders that buyers send. By September it had about nineteen hundred orders in it and had replaced a ledger.

One detail from June mattered more than anything else in this story and nobody noticed it at the time. He had run a migration against the production database directly from his laptop, once, to fix something quickly, and had left the production connection string in his local .env afterwards.

On a Thursday night in September a migration failed halfway. The ledger table showed it as started and not finished, and the tool refused to go any further. He pasted the error. The agent proposed a database reset: the command that drops every table and rebuilds from the migrations. In the material that agent learned from, that is very often the right answer, because in that material it was running against somebody’s laptop.

It ran against the production database at 11:41 at night, and it did exactly what it says it does.

Two details about the approval are worth stating, because neither is unusual. The command appeared in a list of proposed steps with a one-line description that said it would resolve the migration state, which is true. And Karthik had been approving proposed commands for about forty minutes by then, at the rate of one every couple of minutes, which is the rate at which reading stops and clicking starts.

What was in the database: nineteen hundred orders going back fourteen months, the delivery dates the packing table works from, and the buyers’ scanned purchase orders: those last as file addresses, the files themselves being in a bucket and therefore untouched, which he did not know at midnight.

His hosted database was on a free plan with a restore window of about a day, which meant the window applied: the deletion was sixty seconds old. But he had never restored anything. He did not know whether the dump would work, what it contained, or how long it would take, and the page describing it used words he had to look up.

The choice, at a quarter to midnight, was between two costs.

Restoring to 11:35 would bring back the nineteen hundred orders. It would also bring back the state he had been trying to fix (the half-applied migration, still half-applied) so he would be solving the original problem at midnight with less patience than he had at ten.

Rebuilding the schema clean from the migration files would give him a correct, empty database in about a minute and no half-applied anything. The orders would have to be re-entered from the printed picking lists, which the packing table keeps, and which go back nine days.

Twelve days of orders existed nowhere but in the database he had just emptied.

There was a clock on it as well. The restore window is measured from the moment of the deletion, and he did not know whether it was twenty-four hours or six, because the page he was reading described the paid plan in the same paragraph. Whatever he chose, choosing slowly was itself a choice.

And there was his mother, who would open the app at half past six to print the day’s picking list, and who had asked him twice in June why the old ledger had been put away.

How it played out

He restored, and it took eleven minutes. Those eleven minutes were the most useful number he learned that night, because until then the restore was a hope with a duration of unknown. Then he looked at what had actually been applied rather than at what the ledger claimed: the column was there and the ledger said the migration had not finished. One command (the tool’s own resolve, marking it applied) fixed it. The reset had never been the fix for his problem. It was the fix for a problem in somebody else’s training data. In the first week of October he did three things and each is a separate layer. The production connection string came off his laptop entirely and now lives on the host and in a password manager, so his local .env reaches a development database and an agent running in that folder cannot touch real orders whatever it decides. A nightly pg_dump writes to a Backblaze bucket, and he restored one, once, into a scratch database and counted the orders, which is the only thing that turns a backup from a hope into a fact. And the rules file gained a database section forbidding any reset, any push with a force-reset flag, and any DELETE or UPDATE without a WHERE clause. He kept all three, because each of them fails on its own.

Worth arguing about

  1. Three protections are named at the end. Which one would have prevented this night, and why keep the other two?

    One answer

    The credential arrangement. If the production string is not on the laptop, no command the agent runs, however good its reason, can reach real rows, and it is the only layer that works when the rule was not read. The forbidden list stops the routine cases where a reset is the agent’s first suggestion, and it is text, so it can be ignored. The rehearsed backup is what makes the case survivable when both of the others have failed, which is the only situation in which anybody ever needs a backup.

  2. Why is a backup that has never been restored described as a hope rather than a protection?

    One answer

    Because every way it can be useless is invisible until you try. The dump can be of the wrong database, missing a schema, taken with a version the restore refuses, or empty because the connection string in the script pointed at development. All of these have happened to people with a green tick on their backup job. Restoring once also gives you the number Karthik got, eleven minutes, which is the figure you want at the worst possible moment and cannot look up then.

  3. The migration had failed halfway and the tool refused to continue. What was the correct move, and why did the agent not suggest it?

    One answer

    Look at what was actually applied (is the column there or not) and then use the tool’s own resolve command to mark the migration applied or roll it back. It is a five-minute problem when you look. The agent suggested a reset because a reset is the answer that appears most often after this error in public material, and in nearly all of that material the database was a development one with seed data in it, where dropping everything costs nothing. The command is identical; only the database differs, and the model cannot see which one is behind the connection string.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    fs.writeFile(‘./data.json’) works perfectly on your laptop and loses data in production on a serverless host. Why?

  2. 2

    Prisma’s db push and a migration both make the database match your schema file. What does push not do?

  3. 3

    A generated bookings table has a customer name, a start time and a status. Which missing column makes a second user impossible?

  4. 4

    A route reads userId from the JSON body, because the front end helpfully sends it. Every test passes. What is wrong?

  5. 5

    Where does an uploaded photograph belong, and what goes in the database?

  6. 6

    A page is instant with forty bookings and takes thirty seconds with forty thousand. The query log shows 40,001 lines for one page load. What is happening?

Putting a model inside your own app

10 lessons · 1 h 20

You will be able toAdd an AI feature to an app that survives contact with users: call the model only from a server route, cap what one user can spend in three places, validate structured output before anything reads it, explain why prompt injection is defeated by limiting consequences rather than by wording, scope retrieval to the session before ranking, and run a fixed set of inputs before every prompt or model change.

  1. 7.1The key never goes to the browser8 min
  2. 7.2The first call from your own server9 min
  3. 7.3The prompt is a file, not a string7 min
  4. 7.4User text inside a prompt is untrusted9 min
  5. 7.5Structured answers you can render8 min
  6. 7.6Capping what one user can cost you8 min
  7. 7.7When the model is down, slow or empty7 min
  8. 7.8Giving the model your own data10 min
  9. 7.9Is the feature actually good7 min
  10. 7.10Telling users it is AI, and what you send about them7 min
An illustrative case, built from documented patternsFour hundred rupees before breakfastA BSc student in Bhopal running a free revision-notes site for his own batch, in Hindi and English.

Aman Kushwaha’s site does one thing. A student pastes a chapter and gets five bullet points back, in whichever language the chapter was written in. He built it in a weekend, the model call goes through his own server route because that was the one piece of advice he took from a friend, and the route checks the session, so only signed-in students can use it.

Through September and October it cost him nothing. The free tier’s limit is per minute, so a burst simply returned an error and the next request a minute later worked, and the error was free. In early November, with examinations approaching and forty or fifty students using it in the evenings, the per-minute limit started to bite in a way people complained about. So he added a card to the provider account to lift it.

On the fourteenth somebody posted the link in a WhatsApp group of about nine hundred students at a neighbouring college, and usage went up ten times, which was the best day the site ever had.

On the eighteenth, between two and six in the morning, one account made three thousand one hundred calls. Each one carried an input of around six thousand words, which is a whole chapter pasted repeatedly, and no human being was awake to press a button that many times. It was a script, written by somebody for whom generating summaries was cheaper than reading, using a session cookie obtained by signing up.

Aman saw it at seven, on the provider’s usage page, as a number he had to read twice.

He did the arithmetic on the back of a lecture handout, because he had never done it before. Each call sent about eight thousand tokens in and got four hundred back. Three thousand one hundred calls is something over twenty-five million input tokens. He looked up the two prices per million on the provider’s page (input and output are billed separately, and output costs several times more) and the answer came out near four hundred rupees for the night, which is roughly what four evenings of forty students had cost him across the whole of October.

The money was about four hundred rupees. That is recoverable, and it is not the decision.

The decision was what to do before the evening, with examinations nine days away. A per-account cap of twenty summaries a day would make the script useless, and twenty is generous for a person, but not for everybody. Two students had told him they revise ten or eleven chapters in an evening before a paper, and each chapter is often two or three summaries because they paste sections separately. A cap set to stop a script would refuse those students during the week they most needed it, and he would hear about it.

Leaving it open for nine days and watching the usage page meant the card, which is his father’s, stayed exposed to whatever ran overnight while he was writing his own examinations. Watching a page is not a control; it is a plan to notice.

A friend suggested a third option that sounded better than it was: block the account that did it. It takes a minute, it does not refuse any honest student, and it protects against exactly one person who has already demonstrated that signing up is free.

How it played out

He capped at twenty a day per account and put the number where users could see it (you have used 14 of 20 today) which turned a refusal into a feature and produced four polite messages asking for more rather than four bug reports. Two of the four he raised by hand. He also did the two things the per-user cap does not do, and both took less time than the cap did. A hard spending cap of eight hundred rupees a month at the provider, set that night, because an alert tells you after the money has gone and a cap refuses the request. And a maximum input length of eight thousand characters, which made the expensive shape of call impossible regardless of who was calling. There was one correction to make in the code as well: his first version incremented the counter after the response came back, and a burst of parallel requests all read zero at the same instant and all went through. The counter is now claimed atomically before the call and refunded if the provider fails, which is the only order that survives twenty requests arriving in the same second.

Worth arguing about

  1. The route already checked the session. Why did that not stop the script?

    One answer

    Because a session check answers ‘is somebody signed in’, not ‘how much may this person spend’. A script that signs up once has a session cookie and is thereafter indistinguishable from a diligent student, at the speed of a computer. Authentication is the first line of any route and it is not a cost control; the per-user allowance counted in your own table is. The sign-up flood is the version of this that skips even that step, which is why a bot check on the account-creating route sits beside the cap.

  2. Why is a spending alert not a substitute for a spending cap?

    One answer

    An alert emails you after the money has been spent; a cap refuses the request. In this case the difference is between reading a number at seven in the morning and never generating it. The provider’s cap is also the only one of the three limits that holds when every line of your own code is wrong, because it is enforced by the people sending the bill rather than by anything you wrote. Set it before the key exists, at a number you would be annoyed to lose and not devastated.

  3. Aman’s first counter incremented after the response. Describe exactly what a burst of twenty simultaneous requests did to it.

    One answer

    All twenty read the counter before any of them had written to it, so all twenty saw a count below the limit and all twenty proceeded. The limit was checked against a value that was already stale by the time it was read. Claiming the slot first (a single atomic statement that increments and returns the new count) means the twentieth request sees twenty and is refused, and a refund on provider failure keeps the count honest when a call never happened.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    In the Network tab you see a request going from the page straight to a model provider’s host. What have you learned?

  2. 2

    Which of these actually reduces the harm an injected instruction inside user text can do?

  3. 3

    Your validator rejects a category the model invented outside your four-value enum. What should the code do next?

  4. 4

    Twenty requests from one account arrive in the same second and all twenty pass a daily limit of twenty that has already been reached. Where is the mistake?

  5. 5

    A similarity search embeds the question, ranks every note in the table, takes the closest ten, and then drops the ones that do not belong to this user. What is wrong with that order?

  6. 6

    The model provider is down for an hour. Which design decision keeps your app up?

The security holes AI code has by default

10 lessons · 1 h 24

You will be able toAudit a generated app for the holes it has by default: write a single authorisation function and test it with three accounts, find a secret in the built bundle or the git history and revoke it in the right order, locate string-built SQL, shell commands and server-side fetches of user URLs, refuse unknown fields with a strict schema, sanitise at the one prop that injects HTML, limit sign-up and expensive routes with a bot check and per-account counters, detect an upload by its bytes, gate admin routes against an allowlist the app cannot write, and run a per-route checklist plus a free scanner before each release.

  1. 8.1The security holes AI code has by default10 min
  2. 8.2Authorisation beyond ownership9 min
  3. 8.3Secrets in the built bundle, and in the history8 min
  4. 8.4Injection in three places: the query, the shell, the URL9 min
  5. 8.5Validate at the edge, and the field nobody meant to accept8 min
  6. 8.6Cross-site scripting, and the prop with the honest name8 min
  7. 8.7Rate limits, and the sign-up flood8 min
  8. 8.8Uploads that run8 min
  9. 8.9The admin page nobody protected8 min
  10. 8.10Asking the model to audit itself, and what it cannot see8 min
An illustrative case, built from documented patternsThe invoice with the number one in itA three-person accountancy practice in Ahmedabad, with forty small-business clients using a portal built over two weekends.

The portal does one useful thing: a client signs in and sees their own invoices and filings, instead of telephoning to ask whether something has been submitted. The junior partner, Nirav Doshi, built it with an agent over two weekends in the spring, and by autumn all forty clients were using it.

Each invoice has an address like /invoices/71. The numbers run in order, because the table has an auto-incrementing id and nobody thought about it.

In October a client’s son, sixteen and bored in the waiting room, changed 71 to 70 and was shown an invoice belonging to a hardware shop two streets away, including the turnover it was calculated on. He told his father. His father, who is a client, telephoned the practice rather than anybody else, which is the only lucky thing in this case.

The route was thirteen lines. It required a login, and the login worked. It then selected the invoice where the id matched the number in the URL, and returned it. Being signed in had been confused with being allowed, which is not an exotic failure and is present in a large share of applications built this way.

The fix took twenty minutes: one more condition in the query, with the user’s identity taken from the session on the server rather than from anything in the request, and a not-found response when nothing matched.

Before the fix, Nirav did one more thing, and it changed the conversation that followed. He signed in as one client on his laptop and, with a second browser, worked through invoice numbers 1 to 40 by hand. Thirty-one of them returned somebody’s invoice. Nine returned nothing, because those rows had been deleted. It took four minutes, which is the number the senior partner heard first.

The decision took eight days.

The senior partner’s position was that one person had found it, that person’s father had called them, the hole was closed, and there was no evidence anybody else had ever done it. Telling forty small businesses in a town where accountants are chosen by word of mouth that their figures may have been readable by other clients would cost the practice clients, and the practice employs three people.

Nirav’s position was that the phrase ‘no evidence’ was doing work it could not do. There was no logging of refused requests, because there had never been a refusal. The ids ran in sequence, so anybody could have walked the whole set in under a minute, and nothing about that would have looked unusual in any record they kept. He could not say it had happened and he could not say it had not, and the clients were the ones entitled to decide what to do about that.

There was a legal question underneath the reputational one that neither partner was qualified to answer, which is whether a practice holding other businesses’ financial records has a duty to notify anybody when those records were readable, and to whom. The answer depends on where the clients are and what the records contain, and it was moving while they argued about it.

And there was a smaller question that turned out to matter as much. If they said nothing and one client later mentioned it to another over tea (the boy had told his father, and his father had told them) the practice would be explaining not a mistake in a portal but a decision to keep quiet about one.

How it played out

They disclosed, in writing, to all forty within forty-eight hours: what had been possible, for how long, what had been changed, and, stated plainly, that their logs could not show whether anyone else had done it. Two clients asked questions and none left. The engineering that followed took a fortnight of evenings. Ownership checks moved into a single named function that answers whether a given user may perform a given action on a given row, called by every route, because an invoice is genuinely visible to more than one kind of person (the client, the client’s accountant, and the practice) and scattering slightly different conditions across eleven routes is how the next hole is made. Refusals return the same not-found response as a row that does not exist, so an id reveals nothing. New rows use unguessable ids, which does not fix the hole and does stop the enumeration. A thirty-line script now signs in as three accounts before every release (a client, a member of the practice, and a client of nobody) and checks the status code of one request per cell. Nirav’s line in the write-up: twenty minutes to fix, eight days to stop thinking about.

Worth arguing about

  1. The route had a login check and it was not enough. Name the two questions that were being confused, and where the answer to the second must come from.

    One answer

    Are you signed in, authentication, and are you allowed to touch this particular record, authorisation. Generated code very often has the first and not the second. The answer to the second has to be built from a value the caller does not control, which means the user identity read from the session on the server. An id in the URL, a field in the body, or a header the browser set are all things the caller chooses, and a value the caller chooses proves nothing about who they are.

  2. Sequential ids did not create the hole. What did they change?

    One answer

    They changed the cost of exploiting it from finding one leak to harvesting all of them. With ids in sequence, a script walks the whole table in a minute; with unguessable ids, an attacker has nothing to iterate over. This is why the id choice is worth making knowingly, and why swapping to unguessable ids is never the fix. Guessability must not be the thing protecting a row, because a leaked link, a referrer header or a shared screenshot hands the id over and the row is unprotected again.

  3. Why did the practice’s ownership check need to become a function rather than one more condition in each query?

    One answer

    Because most rows are seen by more than one kind of person. An invoice belongs to the client and is also legitimately visible to the practice, so a bare ownership condition is wrong in one direction and gets deleted the first time somebody asks why the accountant cannot see a client’s invoice, leaving nothing. A single named function that answers whether this user may do this action to this row keeps the rule in one file, testable beside it, and changeable in one place when the rule changes.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    A route has requireLogin and then selects the invoice whose id matches the number in the URL. Which test finds the problem?

  2. 2

    A secret scanner reports an API key in a commit from March. What is the first thing to do?

  3. 3

    execFile(‘ffmpeg’, [‘-i’, filename, ‘out.mp4’]) is safe where the same command built as one string is not. Why?

  4. 4

    A profile route runs db.update(users).set(body) with the id taken from the session. What can a request composed by hand add?

  5. 5

    A React page renders a model-written summary through the prop that injects raw HTML. Where does the sanitiser belong?

  6. 6

    Which of these actually protects the route at /api/admin/users?

What it costs, running it, and where this stops

10 lessons · 1 h 24

You will be able toRun a small app for other people at a known monthly cost (say for each service whether it pauses, slows or bills at its limit, catch what throws, what is down and what is merely wrong with three different watchers, turn ‘it doesn’t work’ into a log line and a regression item, write the privacy notice and the deletion path a public site needs, release with an additive migration and a flag, hand the project to a developer with a README that runs) and recognise the specific point at which to learn to code, with a sixty-hour path to get there.

  1. 9.1What it actually costs7 min
  2. 9.2The free tiers, and exactly where they end8 min
  3. 9.3Knowing it broke before users tell you8 min
  4. 9.4The first user complaint8 min
  5. 9.5The legal minimum of a public site9 min
  6. 9.6Shipping changes without breaking what works8 min
  7. 9.7Handing it to a real developer8 min
  8. 9.8Where you have to learn to code9 min
  9. 9.9The learning path from here, with the model as tutor9 min
  10. 9.10Building one real thing10 min
An illustrative case, built from documented patternsThe bug that came back a fourth timeA community library in Kochi with eleven hundred members, whose lending app is maintained by the volunteer who built it.

Meera Nair built the library’s lending app two years ago. Members borrow, renew online, and receive a reminder email two days before a book is due. The library is very fond of it and so is she.

Some members’ due dates are one day early. It was fixed in March, in June, in August, and in November it came back.

Each fix had been at a different layer. In March a timezone conversion was adjusted in the reminder job. In June a plus-one was added where the date is displayed. In August the column was changed to text, because storing it as text meant nothing could convert it. Each fix made the reported cases go away for a while, and none of them was made by anybody who could say what happens to a date between the click and the row.

Meera can describe four of the six symptoms from this course’s closing lesson honestly. The same bug returns wearing different clothes. She cannot say, without looking, what happens when a member presses Renew. She cannot describe the data in one paragraph. She is afraid to touch the reminder job.

The November report is what made it undeniable. A member came to the desk with a printed email saying a book was due on the fourteenth, and a receipt from the app saying the fifteenth. Same book, same member, two dates produced by the same system on the same day. The librarian charged no fine and apologised, which she had also done in March, in June and in August, and which is the part Meera minds.

She had asked the model for help each time and each time it had helped. That is the uncomfortable observation. The explanations were coherent, the changes were small, and every one of them was a change to a place where the wrong date appears rather than to the place where it is made, because the model was shown a symptom and a file, never the path, and there was nobody in the conversation who could describe the path.

One of these is normal. Four means that more prompting will not help, and there are three real choices.

Learning enough herself is sixty hours (an hour a day for two months) during which members keep receiving reminders for the wrong day and the librarian keeps apologising at the desk.

Bringing somebody in has a number attached. A developer she found through a friend quoted eighteen thousand rupees for two days, which is most of the library’s annual technology budget and would have to be justified to a committee that believes the app was free, because nobody ever invoiced for it.

Keeping it small means switching off the reminder emails. That removes the bug at once, and removes the feature members mention most often, which is also the feature that got the app adopted in the first place. The library’s overdue rate before reminders was about one book in six.

Each option also carries a cost that does not appear on its own side of the ledger. Sixty hours is sixty hours she is not spending on the reading programme she volunteers for. Eighteen thousand rupees buys the fix and buys nothing that stops the next one, because the person who understands the app afterwards will still not be anybody at the library. And switching the reminders off is the option that looks free and quietly makes the app a little less worth having, one feature at a time, which is how a project stops mattering without anybody deciding that it should.

How it played out

She refused the framing that these were alternatives and took the third and the first, in that order. The reminders went off on the Tuesday behind an environment-variable flag (off, not deleted, so it could come back without a deploy) which stopped the wrong emails within the hour and cost the feature honestly rather than silently. Then five weeks on the first three items of the learning path, an hour most mornings, with the model told to explain and to ask her questions rather than to write anything. In week four, tracing one renewal out loud, she found it. The due date was written as text formatted in the browser’s local time, and the reminder job formatted it again in UTC. India is five and a half hours ahead, so any date recorded between midnight and half past five in the morning falls on the previous day in UTC, and only those members were affected, which is why it looked intermittent, and why three fixes had each ‘worked’ for whoever happened to be checked. The column is now a timestamp with a timezone, converted once at the edge, with a test that renews at two in the morning. She did not hire the developer. She put the quotation into the committee minutes as the cost of the alternative, which is what made sixty hours easy to approve.

Worth arguing about

  1. Three fixes made the reported cases disappear and none of them was a fix. What distinguishes them from the fourth?

    One answer

    Each of the first three removed a symptom without removing the cause: a conversion adjusted, a plus-one added, a column made untypeable so nothing could convert it. The behaviour changed for the members somebody happened to check, and the underlying mistake (the date being formatted twice, in two different zones) was untouched. The check that would have caught all three is to undo the fix and confirm the bug returns, and then to ask which line caused the error and which line prevents it now. A vague answer to that second question is the signal that nobody knows.

  2. Why did the bug look intermittent, and what does that tell you about the test that should now exist?

    One answer

    Because only renewals recorded between midnight and half past five in the morning local time fall on the previous day in UTC, which is a small and unremarkable slice of members. Any test performed during working hours passes. The permanent check therefore has to be the specific case that fails: a renewal at two in the morning, with the expected due date written down. Every bug that took more than twenty minutes deserves that treatment, because the reproduction and the expected result are the two hard parts and you already have both.

  3. Meera treated three options as one plan. When would hiring the developer have been the better first move?

    One answer

    When the cost of the wrong answer is borne by somebody who did not choose the risk: other people’s money, health data, anything involving children, anything with a legal duty. A library’s due dates are annoying and reversible, so the slower route was affordable. It would also have been the right move if the library had needed the reminders running throughout, because the flag would not have been available as a way to buy time. Bringing somebody in is one of the three legitimate answers, and the one that stops being optional as the consequences leave your own hands.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    Why is a spending alert not a substitute for a hard spending cap at the provider?

  2. 2

    Your database provider suspends compute after a few idle minutes and wakes it on the next connection. What does that need from your app?

  3. 3

    Every request succeeds and the totals on the invoice page are wrong. Which watcher catches this?

  4. 4

    Which change turns a message saying ‘it doesn’t work’ into a specific log line in seconds?

  5. 5

    You rename a column and deploy the code that uses the new name in the same release. What happens in the gap between the migration and the new code serving traffic?

  6. 6

    What is the real limit on how far software built this way can go?

What this course is

People who cannot really write code are shipping working software with Cursor, Claude Code, Replit, Lovable and Bolt. This course teaches that skill honestly: what the tools actually do, how to describe what you want, how to read code well enough to trust it, how to recover when the model breaks your app, git and deploying for people who have never done either, what it really costs, the security holes AI code has by default, and the point where you must learn to code properly or stop.

Who wrote it

Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.

More in Build with it

  1. Python, from Zero, for AI89 lessonsFrom your first line of code to your first API call.
  2. Building with AI80 lessonsFrom your first API call to a feature you can trust
  3. Agents and Automation72 lessonsWhat an agent actually is, and when you should build a script instead.
  4. Shipping It74 lessonsGet what you built in front of real people, and keep it alive.
  5. Data, SQL and Getting to the Answer66 lessonsMost AI problems turn out to be data problems. This is where you learn to ask a database a question and defend the answer.
  6. Knowing If It Works76 lessonsThe least glamorous skill in applied AI, and the one that decides whether your thing actually works.
  7. Context Engineering87 lessonsThe window is a budget. Learn to spend it.