{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"seed-data-and-the-empty-state","title":"Seed data, and the empty state nobody tested","url":"https://addaly.com/learn/building-apps-with-ai/seed-data-and-the-empty-state","position":51,"of":87,"minutes":7,"answer":"Development with a full database hides the empty state every new user meets first, so seed on purpose and test with zero, one and ten thousand rows.","keyIdea":"Development with a full database hides the empty state every new user meets first, so seed on purpose and test with zero, one and ten thousand rows."},"body":{"format":"markdown","text":"## Two databases, two worlds\n\nYour development database has your account, the three test bookings you made on Tuesday, and whatever the agent inserted while checking its work. It has never been empty since the first hour, and neither has your view of the app.\n\nA new user's world is a table with no rows. The dashboard they see first is the one you have not looked at since the project began, and a large share of \"it works for me\" reports are this: code written and checked against a full database, meeting an empty one.\n\n## Seeding on purpose\n\nA seed script inserts known data into a development database so that a fresh clone, a reset, or a collaborator's machine starts with something to look at:\n\n```typescript\n// scripts/seed.ts\nawait db.insert(users).values([{ id: DEMO_USER, email: \"demo@example.com\" }]);\nawait db.insert(bookings).values(\n  Array.from({ length: 40 }, (_, i) => ({\n    userId: DEMO_USER,\n    startsAt: addDays(new Date(), i - 20),\n    status: [\"pending\", \"confirmed\", \"cancelled\"][i % 3],\n  }))\n);\n```\n\nMake it produce the awkward cases as well as the pretty ones: a booking in the past, one today, a cancelled one, a name with an apostrophe, a user with no bookings at all. The seed is the cheapest test data you will ever write, and the agent will use it too when it checks its own work.\n\nSeed only development. The script should refuse to run if the connection string looks like production: a check on the hostname takes one line and has saved real data. A seed against production either fills a live app with `demo@example.com` or, in the common version where the script clears tables first, empties it.\n\n## Zero, one, many\n\nTest every list at three sizes.\n\n**Zero.** What does the page show? A generated page usually shows nothing, or crashes on `rows[0].name`. An empty state is a design decision: a sentence saying what will appear here and a button to make the first one. It is the first screen every new user sees, so it is the most-viewed screen you have.\n\n**One.** Pluralisation (\"1 bookings\"), layouts that assumed a grid, summaries that divide by a count.\n\n**Many.** Ten thousand rows through a page that renders all of them. Generated code selects everything and maps over it; at 40 rows it is instant, at 40,000 the page takes fifteen seconds and the browser tab freezes. The seed script can generate ten thousand rows in a loop in under a second, and then you see the problem before a user does. Pagination is the answer and a later lesson covers it.\n\n## The null in the middle\n\nBetween zero and one lives the row that exists with a field missing: a booking with no phone number, a user who signed up with Google and has no name. The nullable columns from the previous lesson each produce one of these, and the seed should include them, because `null` rendered on screen as the word \"null\" is what a user sees when nobody tested the row with the gap.\n\n## Copies of production are personal data\n\nAt some point somebody suggests loading a copy of the real database locally \"to test with realistic data\". The realism is real; so is the fact that a copy of every customer's name, email and history is now on a laptop, in an agent's context window, possibly in a chat transcript.\n\nIf you need production shape, export it with the personal columns replaced: names from a word list, emails as `user-1042@example.com`, free text dropped. Tools exist for this, and a script the agent writes can do it in an afternoon. Then the copy is a copy of the shape and the volume, which is what you wanted, and not of the people.\n\n## The test accounts\n\nKeep two accounts in development beyond your own: one with a lot of data and one with none. Sign in as the empty one before every release and click through the main path. It takes two minutes and it is the check that catches the blank dashboard, the crash on the first element, and the sentence that assumed you already had something.\n\nThe demo account is also what you hand to anybody testing the app, which means it must never be an account with a real person's data behind it, and its password must not be the one you use anywhere else, because you will type it in front of people."},"check":{"question":"A dashboard page works for the developer but shows a blank white screen to a brand-new user. The console reports 'Cannot read properties of undefined (reading 0)'. What is the most likely cause?","options":[{"text":"The new user's session has expired.","correct":false,"tempting":"An expired session redirects to sign-in or returns 401; it does not produce an undefined array element."},{"text":"The database is not connected in production.","correct":false,"tempting":"A connection failure errors at the query, with a message about the connection, not at reading the first element of a result."},{"text":"The page reads the first row of a list without handling the case where the list is empty, and the developer never saw it empty.","correct":true},{"text":"The seed script ran against production and corrupted the data, so the new user's rows are being read from a table whose shape the page does not expect.","correct":false,"tempting":"A seeded production database would show the new user rows they should not see, a different and worse failure than an empty list."}]},"prev":{"slug":"the-columns-every-table-needs","title":"The columns every table needs","url":"https://addaly.com/learn/building-apps-with-ai/the-columns-every-table-needs"},"next":{"slug":"sign-in-without-writing-it-yourself","title":"Sign-in, without writing it yourself","url":"https://addaly.com/learn/building-apps-with-ai/sign-in-without-writing-it-yourself"},"authorship":"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.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Seed data, and the empty state nobody tested', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/seed-data-and-the-empty-state (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Seed data, and the empty state nobody tested","courseTitle":"Building Apps with AI","module":{"index":6,"slug":"the-parts-that-persist","title":"The parts that persist: data, accounts and files"},"answer":null,"keyIdea":"Development with a full database hides the empty state every new user meets first, so seed on purpose and test with zero, one and ten thousand rows.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"two-databases-two-worlds","runs":[{"text":"Two databases, two worlds"}]},{"t":"p","runs":[{"text":"Your development database has your account, the three test bookings you made on Tuesday, and whatever the agent inserted while checking its work. It has never been empty since the first hour, and neither has your view of the app."}]},{"t":"p","runs":[{"text":"A new user’s world is a table with no rows. The dashboard they see first is the one you have not looked at since the project began, and a large share of “it works for me” reports are this: code written and checked against a full database, meeting an empty one."}]},{"t":"h2","id":"seeding-on-purpose","runs":[{"text":"Seeding on purpose"}]},{"t":"p","runs":[{"text":"A seed script inserts known data into a development database so that a fresh clone, a reset, or a collaborator’s machine starts with something to look at:"}]},{"t":"code","lang":"typescript","label":"TypeScript","text":"// scripts/seed.ts\nawait db.insert(users).values([{ id: DEMO_USER, email: \"demo@example.com\" }]);\nawait db.insert(bookings).values(\n  Array.from({ length: 40 }, (_, i) => ({\n    userId: DEMO_USER,\n    startsAt: addDays(new Date(), i - 20),\n    status: [\"pending\", \"confirmed\", \"cancelled\"][i % 3],\n  }))\n);"},{"t":"p","runs":[{"text":"Make it produce the awkward cases as well as the pretty ones: a booking in the past, one today, a cancelled one, a name with an apostrophe, a user with no bookings at all. The seed is the cheapest test data you will ever write, and the agent will use it too when it checks its own work."}]},{"t":"p","runs":[{"text":"Seed only development. The script should refuse to run if the connection string looks like production: a check on the hostname takes one line and has saved real data. A seed against production either fills a live app with "},{"text":"demo@example.com","code":true},{"text":" or, in the common version where the script clears tables first, empties it."}]},{"t":"h2","id":"zero-one-many","runs":[{"text":"Zero, one, many"}]},{"t":"p","runs":[{"text":"Test every list at three sizes."}]},{"t":"p","runs":[{"text":"Zero.","b":true},{"text":" What does the page show? A generated page usually shows nothing, or crashes on "},{"text":"rows[0].name","code":true},{"text":". An empty state is a design decision: a sentence saying what will appear here and a button to make the first one. It is the first screen every new user sees, so it is the most-viewed screen you have."}]},{"t":"p","runs":[{"text":"One.","b":true},{"text":" Pluralisation (“1 bookings”), layouts that assumed a grid, summaries that divide by a count."}]},{"t":"p","runs":[{"text":"Many.","b":true},{"text":" Ten thousand rows through a page that renders all of them. Generated code selects everything and maps over it; at 40 rows it is instant, at 40,000 the page takes fifteen seconds and the browser tab freezes. The seed script can generate ten thousand rows in a loop in under a second, and then you see the problem before a user does. Pagination is the answer and a later lesson covers it."}]},{"t":"h2","id":"the-null-in-the-middle","runs":[{"text":"The null in the middle"}]},{"t":"p","runs":[{"text":"Between zero and one lives the row that exists with a field missing: a booking with no phone number, a user who signed up with Google and has no name. The nullable columns from the previous lesson each produce one of these, and the seed should include them, because "},{"text":"null","code":true},{"text":" rendered on screen as the word “null” is what a user sees when nobody tested the row with the gap."}]},{"t":"h2","id":"copies-of-production-are-personal-data","runs":[{"text":"Copies of production are personal data"}]},{"t":"p","runs":[{"text":"At some point somebody suggests loading a copy of the real database locally “to test with realistic data”. The realism is real; so is the fact that a copy of every customer’s name, email and history is now on a laptop, in an agent’s context window, possibly in a chat transcript."}]},{"t":"p","runs":[{"text":"If you need production shape, export it with the personal columns replaced: names from a word list, emails as "},{"text":"user-1042@example.com","code":true},{"text":", free text dropped. Tools exist for this, and a script the agent writes can do it in an afternoon. Then the copy is a copy of the shape and the volume, which is what you wanted, and not of the people."}]},{"t":"h2","id":"the-test-accounts","runs":[{"text":"The test accounts"}]},{"t":"p","runs":[{"text":"Keep two accounts in development beyond your own: one with a lot of data and one with none. Sign in as the empty one before every release and click through the main path. It takes two minutes and it is the check that catches the blank dashboard, the crash on the first element, and the sentence that assumed you already had something."}]},{"t":"p","runs":[{"text":"The demo account is also what you hand to anybody testing the app, which means it must never be an account with a real person’s data behind it, and its password must not be the one you use anywhere else, because you will type it in front of people."}]}],"check":{"question":[{"text":"A dashboard page works for the developer but shows a blank white screen to a brand-new user. The console reports ‘Cannot read properties of undefined (reading 0)’. What is the most likely cause?"}],"options":[{"text":[{"text":"The new user’s session has expired."}],"correct":false,"tempting":[{"text":"An expired session redirects to sign-in or returns 401; it does not produce an undefined array element."}]},{"text":[{"text":"The database is not connected in production."}],"correct":false,"tempting":[{"text":"A connection failure errors at the query, with a message about the connection, not at reading the first element of a result."}]},{"text":[{"text":"The page reads the first row of a list without handling the case where the list is empty, and the developer never saw it empty."}],"correct":true,"tempting":null},{"text":[{"text":"The seed script ran against production and corrupted the data, so the new user’s rows are being read from a table whose shape the page does not expect."}],"correct":false,"tempting":[{"text":"A seeded production database would show the new user rows they should not see, a different and worse failure than an empty list."}]}],"order":[0,1,2,3],"why":null},"prev":{"slug":"the-columns-every-table-needs","title":"The columns every table needs","minutes":7},"next":{"slug":"sign-in-without-writing-it-yourself","title":"Sign-in, without writing it yourself","minutes":9}}}