How databases work in AI-built apps
A database stores your app's data in tables: fixed columns, one row per thing. Your app reads and writes rows through queries. In an AI-built app the tables, the relationships between them, and the rules about who can touch which row were all generated for you — and the rules are the part worth reading first.
What is a table, really?
A spreadsheet with enforced structure. Each column has a type, so a date column cannot hold 'next Tuesday'. Each row usually has an `id` that nothing else shares, which is how other tables refer to it.
| id | user_id | title | created_at |
|---|---|---|---|
| c1f... | 9ab... | Shopping list | 2026-09-20T10:04Z |
| d2e... | 9ab... | Launch plan | 2026-09-21T16:20Z |
The `user_id` column is doing the important work here: it is what makes a row belong to somebody. Almost every access rule you will ever write depends on a column like it.
How do tables relate to each other?
By storing each other's ids. A note holds the id of its author; an order holds the id of its customer. That reference is called a foreign key, and the database can enforce it — refusing to store a note whose author does not exist, or deleting notes when their author is deleted.
- One-to-many: one user has many notes (the note holds the user id).
- Many-to-many: users belong to many teams and teams have many users (a third table holds the pairs).
- One-to-one: a user has one profile row (often used to keep public fields separate from account fields).
What is a query doing?
-- SQL
select id, title from notes where user_id = $1 order by created_at desc limit 20;
// Client library
supabase.from("notes").select("id,title").eq("user_id", userId)
.order("created_at", { ascending: false }).limit(20);Every query answers four questions: which table, which columns, which rows, in what order. If a list on your page is wrong, one of those four is wrong — and it is nearly always 'which rows'.
Why does the database say no?
Modern hosted databases enforce row-level access rules. A query that is perfectly valid will still return nothing if the rules do not allow this particular user to see those rows. Crucially, this often looks like an empty list rather than an error, which is why people spend hours debugging the wrong layer.
An empty list with no error is a permissions symptom until proven otherwise. Check the rules before you check the query.
What should you check in an AI-generated schema?
- 1Does every table holding personal data have a column identifying the owner?
- 2Are access rules enabled on every table, and does each rule name a real condition rather than allowing everyone?
- 3Are permissions granted to the roles your app actually uses?
- 4Do timestamps exist so you can tell when something was created or changed?
- 5Is anything stored that you would not want to explain in a privacy policy?
Frequently asked questions
- Do I need to learn SQL?
- Not to build, but reading it is worth an afternoon. Almost every serious debugging session ends with someone reading a query.
- Where is my data actually stored?
- On your database provider's servers, in a region you chose or were assigned. It is not inside your app or your hosting deployment.
- What happens to data when I redeploy?
- Nothing. Code and data are separate; only a migration changes the database.
Practice this in MessyDev
Reading it once helps. Doing it once sticks. These are the hands-on parts of MessyDev that cover the same ground.
Keep going
- Row Level Security explainedWhat Row Level Security is, why it is on by default in hosted databases, how a policy is evaluated, and the mistakes that make tables silently empty or dangerously open.
- Why can't my frontend access my database?Empty lists, permission errors and silent failures between the browser and a hosted database — and how to tell which of the three you have.
- Authentication vs authorizationTwo words one letter apart that solve different problems: proving who someone is, and deciding what they may do. Plus how each one fails.