- Core
- 100 min
- 13 min read
- You marked this lesson done
- Updated
After this lesson you can
- Explain tables, keys, constraints, transactions and migrations without jargon
- Read a small schema and say whether it protects the product's promises
- Review a migration for risk to data, and ask for a backup and restore test
- Say why database access lives in one package and what that protects
Read these first
This is lesson 4 of the PHANTOMS track. It supports the Master Developer modules on platform and production launch, and Developer week 6, where your demo must work on real data. P is the first letter and the one you may never skip: a product that loses its records has lost its customers’ trust.
1. Why this matters
Everything a customer cares about is a record: an order, a payment, a cup lent, a cup returned. If the record is wrong or gone, the promise is broken, whatever the interface looks like.
A database is a program whose whole job is to keep records safe and answer questions about them. PostgreSQL (often called Postgres) is a free, open source, relational database that has been in use for decades. “Relational” means the data lives in tables, like spreadsheets with strict rules, and tables can point to each other.
The 2030 Product Development Manager does not write SQL all day. But they must be able to look at a data model and ask: does this protect the promise we made to the customer? Agents will create tables quickly. Only you know that an order must never exist without a café, or that a delivered order must not go back to “placed”.
The starter calls this the durable relational data and transactions layer. The word durable is the point: if the power fails one second after the customer clicks “order”, the order is still there.
| The 2030 PDM decides | Delegates to agents | Verifies by | Other disciplines |
|---|---|---|---|
| What must always be true about the data, how long to keep it, who may see it, what loss is acceptable | Writing table definitions, migrations, queries and tests | Reading the schema against the rules, checking the migration, asking for a restore test | Security (who can read what), legal (personal data, retention), finance (storage and backup cost), operations (recovery) |
2. Key ideas, explained
Tables, rows, keys
A table holds one kind of thing: orders, cafés, cups. A row is one item. A column is one fact about it. A primary key is a column (or set) that identifies a row uniquely. A foreign key says “this value must match a row in another table”, for example every order points to a café that exists.
The starter’s sample table is called tasks. It has an id (a UUID, a long random identifier), a title of up to 200 characters that cannot be empty, and a creation time stored with time zone. Small, but already making three good decisions: a safe id, a limit, and unambiguous time.
Constraints: the database says no
A constraint is a rule that the database itself enforces. Examples: NOT NULL (a value is required), CHECK (a value must meet a test), UNIQUE, and foreign keys. This matters because application code has bugs and agents write application code. A constraint is the last line of defence: even a buggy program cannot store a bad record.
Rule of thumb for review: every promise that must never be broken should be a constraint, not only a line of application code. The PostgreSQL manual’s chapter on constraints is short and worth reading.
Transactions: all or nothing
A transaction groups several changes so that either all of them happen or none do. If a customer places an order with three line items, you do not want a crash to leave the order with one item. Postgres guarantees that committed transactions survive a crash. This is what “durable” means. You will not write transactions by hand often, but when you review code that changes several tables, ask: is this inside one transaction?
Migrations: changing the shape safely
A product changes. A column is added, a rule is tightened. A migration is a small, numbered file that describes one change to the database’s structure. Migrations live in git and run in order, so every environment ends up with the same shape. The starter uses a SQL-first TypeScript library called Drizzle for this. The workflow, from the root scripts:
| Step | Command | What happens |
|---|---|---|
| Describe the change | edit packages/db/src/schema.ts |
The desired shape, in TypeScript |
| Generate the migration | bun run db:generate |
A SQL file appears in packages/db/drizzle/ |
| Review the SQL | read it | You or a person you trust reads the generated SQL |
| Apply it | bun run db:migrate |
The database changes |
| Check consistency | bun run db:check |
The migration history is consistent |
The starter’s rule: “use reviewed migrations for schema changes.” The word reviewed matters. A migration that drops a column deletes data. Generated SQL is a proposal. Before you apply a migration to a database that holds valuable data, a person reads it. Plan for a way back, or a forward fix, in case a change goes wrong. We fix forward (the Way): a new migration repairs the problem, we do not rewrite history.
One package owns the database
In the starter, all database code lives in packages/db: the connection, the schema and the queries. The API calls functions from it, and does not talk to the database directly. AGENTS.md says: “keep database access in packages/db”. Why? So you have one place to review, one place to change when you swap a library, and one place to enforce rules such as connection limits and timeouts. When an agent writes a database query inside a route handler, it breaks this boundary. Reject it.
Local database, production database
Locally, the starter runs PostgreSQL in a container with one command (docker compose up -d postgres). The container listens only on the local machine (127.0.0.1), and the password in .env.example is a placeholder for a disposable database. In production the rules change: use a managed or carefully run database, a strong secret from a secret manager, TLS, and an approved image identified by its digest (an exact fingerprint), not just a version tag. The version policy lists PostgreSQL 18.6 for the local container only.
3. Step by step: how to review a data model
Use this procedure when an agent proposes a schema or a migration.
- List the nouns and promises. Write in plain words what the product stores and what must always be true. “Every order belongs to one café.” “An order has one status.” “A delivered order is never changed back.”
- Read each table. For each column ask: is it required? Is there a limit? Is the type right? Money should not be a floating-point number. Times should have a time zone.
- Match promises to constraints. For each promise, find the constraint that enforces it. If there is none, ask for one, or for a written reason.
- Check relationships. Foreign keys exist where one record depends on another. Deleting a café does what you intend to its orders: block, or remove.
- Check personal data. What fields identify a person? Do you need them? Who can read them? How long do you keep them?
- Read the migration SQL. Look for
DROP,DELETE,TRUNCATEor a type change. Each needs a reason and a safe plan. - Ask for the evidence. Tests run against a disposable PostgreSQL, a migration applied to a fresh database, and a statement of how restore was tested.
- Ask who may connect. The app uses a database account with only the rights it needs, separate from the account that applies migrations. The security guide asks for exactly this.
4. Worked example: TrailMug
Maya’s order tracker needs an orders table. She writes the promises herself:
- Every order belongs to one café.
- An order has a cup count that is a whole number above zero.
- Status is one of: placed, packed, shipped, delivered.
- We keep the café’s name and a delivery contact, but no home addresses.
She briefs an agent and reviews the SQL that comes back. A simplified version looks like this:
create table orders (
id uuid primary key default gen_random_uuid(),
cafe_name varchar(120) not null,
cup_count integer not null check (cup_count > 0),
status varchar(20) not null default 'placed'
check (status in ('placed','packed','shipped','delivered')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
This is an illustration, not the starter’s code. Maya reads it against her promises. Cup count above zero: constraint, good. Status in a fixed list: constraint, good. Café: stored as a name, not a link to a cafés table. She notes that two cafés may share a name, and asks the agent to add a cafes table and a foreign key. The agent does. She also asks: can a delivered order move back to placed? The schema alone does not stop that. She decides it is a rule for the API and tests, and writes it into the acceptance criteria for the status-change feature.
Sam, the operations lead, asks the question managers forget: “If we lose the database tonight, how do we get orders back?” Maya adds an epic called “Backups and restore drill” to the roadmap. Writing it herself, not an agent. Until a restore has been tested, she writes in the README, the tracker is a demo, not a system of record.
5. Working with AI agents on this
What to delegate. Drafting table definitions from your promises, generating the migration, writing queries inside packages/db, writing tests that run against a disposable database, explaining what a SQL statement does.
What you must do yourself. Write the promises. Decide retention and what personal data you keep. Read the migration. Decide who gets database credentials. Own the backup and restore plan.
A prompt you can adapt:
Work in the PHANTOMS starter. Read AGENTS.md, docs/architecture.md and docs/security.md first. Task: replace the sample
taskstable with anorderstable that meets these promises:. Rules: all database code stays in packages/db; use the existing Drizzle workflow (bun run db:generate); every promise that must never break is a database constraint, not only application code; keep the existing migration history, add a new migration instead of editing an old one. Show me the generated SQL before you apply it, and explain each line in one plain sentence. Add tests that run against the disposable local PostgreSQL. Run the checks listed in AGENTS.md and paste the results. Do not log personal data.
How to check the output. Read the SQL line by line against your promises. Look for what is missing: a limit, a time zone, a relationship, an index for the lookup you expect to be common. Run the migration on a fresh disposable database. Ask for a test that tries to insert a bad row and expects a refusal.
Disclose. Say in the merge request that an agent generated the schema and that you verified each promise against a constraint. See disclose AI use.
6. Common mistakes and how to avoid them
- Trusting application code alone. Make the database enforce the important promises.
- Applying generated migrations without reading them. A harmless-looking change can delete a column. Read every migration.
- Editing an old migration. History must not change. Add a new migration.
- Using the local password or database in production. The local
.envvalues are for disposable use only. - Giving the app the migration account. Use least-privilege accounts: one for the app, one for migrations.
- Never testing a restore. A backup you have not restored is a hope. Schedule a drill.
- Keeping personal data “just in case”. Store what the promise needs. Delete when the retention rule says so.
- Letting authentication stand in for authorization. Knowing who the user is does not tell you which orders they may see. Check each lookup against user and café.
7. Practice
Exercise: “Promises first, schema second.”
- Write five promises about the data for a product you care about, or for TrailMug’s orders. One page, your words.
- In a clone of the public starter, brief an agent with the prompt above on a branch.
- Read the generated SQL. Make a table with two columns: “my promise” and “constraint that enforces it, or ‘none’”.
- For each “none”, decide: ask for a constraint, or write the rule as an API test, or accept the risk and say why.
- Write down the restore question: what is the first step you would take to prove you can recover the data? You do not have to build it yet.
Deliverable in git: docs/data-promises.md (your promises and the constraint table), the migration, and a merge request that says what the agent did and what you changed after review.
8. Checklist
- I wrote the data promises in my own words.
- Every must-never-break promise maps to a constraint, or I recorded why not.
- I read the migration SQL, especially anything that removes or changes data.
- Database code lives only in
packages/db. - The app and the migrations use different, least-privilege accounts in production.
- A restore test is on the roadmap, written by me.
- I know what personal data is stored and why.
9. Further learning
Start with the official PostgreSQL tutorial. It is clear and free. Then read the constraints and backup chapters, which map directly to the promises and the restore drill above. PG Exercises gives you hands-on practice for reading agent-written queries, and the freeCodeCamp video is a good option if you prefer to watch first. The Drizzle overview explains the library the starter uses for the schema and migrations.
Next: H is for Hono.
Finished this lesson?
This is only for you. It is saved in this browser, on this device, and nothing is sent anywhere. It is not a grade and not a certificate.
Go deeper
Further learning
Free or low-cost material from other publishers. We opened every link; check the cost label before you start.
PostgreSQL Global Development Group
PostgreSQL Documentation: Part I. Tutorial (opens the publisher's site in a new tab)
- Free
- Tutorial
- Beginner
- about 2 hours
The official PostgreSQL tutorial: tables, queries, joins, updates and transactions, with small examples. It is the best first read for anyone who must understand the data an agent creates.
PostgreSQL Global Development Group
PostgreSQL Documentation: Constraints (opens the publisher's site in a new tab)
- Free
- Docs
- Intermediate
- about 30 minutes
The official guide to primary keys, foreign keys, NOT NULL and CHECK rules. Read it to see how the database itself can refuse bad data, whatever the app code does.
PostgreSQL Global Development Group
PostgreSQL Documentation: Backup and Restore (opens the publisher's site in a new tab)
- Free
- Docs
- Intermediate
- about 45 minutes
The official chapter on dumping, file-level backup and point-in-time recovery. Read it to learn the questions to ask before a product keeps real customer data.
PGExercises
PostgreSQL Exercises (opens the publisher's site in a new tab)
- Free
- Interactive
- Beginner
- several hours
A free site of graded SQL questions you answer in the browser, from simple selects to joins and aggregates. Practise reading and writing queries so you can check an agent's SQL.
freeCodeCamp.org (YouTube)
Learn PostgreSQL Tutorial - Full Course for Beginners (opens the publisher's site in a new tab)
- Free
- Video
- Beginner
A long video course from the freeCodeCamp.org channel that walks through installing PostgreSQL and writing SQL from scratch. Watch it if you prefer to see the commands run before you read the manual.
Drizzle Team
Drizzle ORM - Why Drizzle? (opens the publisher's site in a new tab)
- Free
- Docs
- Intermediate
- about 20 minutes
The overview of Drizzle ORM, the SQL-first TypeScript library the PHANTOMS starter uses for its database package. Read it to understand schema files and migrations before you review an agent's database change.



