Your database is where all your data lives — every user, every purchase, every conversation. It is the one thing that is hardest to change later, so getting the basics right early matters more than getting them perfect.
If you can use a spreadsheet, you can understand a database. A database is just a collection of spreadsheets (called "tables") that can reference each other.
Your Users table has a row for every person who signed up — their name, email, when they joined. Your Subscriptions table has a row for every subscription — which user it belongs to, which plan they are on, when it started. The two tables are connected: every subscription row points to a user row through a shared ID number.
That connection is the superpower. "Show me all the subscriptions for this user" is one simple question to the database. "How many users are on the Pro plan?" is another. "Which users signed up this week but have not subscribed yet?" is a third. The database answers all of these instantly because the relationships between tables are built in.
A structured collection of data with rows and columns — exactly like a spreadsheet. Each table holds one type of information: Users, Subscriptions, Payments, Messages. Each row is one entry. Each column is one piece of information about that entry.
A column in one table that points to a row in another table. If your Subscriptions table has a "user_id" column, that is a foreign key — it tells you which user this subscription belongs to. It is the thread that connects your tables together.
Each table connects to the next through foreign keys — user_id links subscriptions to users, subscription_id links payments to subscriptions.
Your database is the filing system in the back office. One cabinet for Customers, one for Reservations, one for Suppliers. Each file in the cabinet has the same fields filled out. The customer's reservation card has their customer number on it — pull that number, find their full file. That is a foreign key.
By default, if your app can read from your database, it can read everything. Every user's data. Every other user's data too. That is fine during development when it is just you. It is a serious problem when real people are using your product.
Row-level security (RLS) is a feature in modern databases like Supabase and PostgreSQL that enforces a simple rule: User A can only see and change User A's data. User B can only see and change User B's data. This is enforced at the database level — meaning even if there is a bug in your app code, the database itself blocks unauthorized access.
Set this up on day one, not after your first data privacy incident. It takes an hour and it is one of those things where doing it early is trivially easy and doing it later is surprisingly painful.
A database feature that controls who can see which rows. Think of it as individual locks on every filing cabinet drawer. The receptionist can open the Reservations drawer. The accountant can open the Finances drawer. A customer can only see their own file. Even if someone picks the wrong door, the drawer will not open.
Any user can query any other user's data. A single API bug exposes your entire database. You discover this when a user sees someone else's orders — or worse, when a security researcher publishes it.
The database itself enforces boundaries. Even a buggy query can only return the current user's rows. The lock is on the data, not just the app code that accesses it.
Many database platforms (including Supabase) return a maximum of 1,000 rows by default. It does not tell you it stopped. Ask for "all users" with 5,000 in the table and you get 1,000 back — your dashboard, your reports, your investor conversations are all based on bad data. Nothing looks broken.
Never pull rows and count them in your app. Use server-side aggregation — ask the database to count for you directly. Always returns the correct answer.
Is it "user_id" or "userId"? Is it "created_at" or "createdAt"? AI guesses — and guesses wrong about 30% of the time. A wrong column name does not crash your app. It returns empty results, which your app interprets as "no data" instead of "that column does not exist."
Always verify actual column names in your database before writing queries. Copy-paste, do not type from memory or let AI guess.
Asking the database to do the math instead of doing it yourself. Instead of "give me all 5,000 rows and I will count them" (which silently caps at 1,000), you say "count the rows and tell me the number." The database does the work and gives you the correct answer: 5,000.
// BAD — silently capped at 1,000 rows const { data } = await supabase .from('users') .select('*') // data.length === 1000 (you have 5,000 users) // GOOD — always correct const { count } = await supabase .from('users') .select('*', { count: 'exact', head: true }) // count === 5000 ✓
If your aggregate numbers come from SELECT queries without server-side COUNT, your data is probably wrong and you do not know it yet.
This is not a failure. It is expected. You will add columns, rename things, and restructure tables as you learn what your product actually needs. The first version of your database is a hypothesis, not a final answer.
What matters is knowing how to make changes safely.
Safe. Existing rows get a blank value in the new column. No existing code breaks.
Risky. Any code that references the old name will break. Check all references first.
Permanent. The data is gone. Make sure nothing needs it before you remove it.
Additions are safe. Renames require checking what references the old name first. Deletions are permanent. And always, always test changes in sandbox first (Chapter 7).
Most managed database platforms include automatic daily backups on paid plans. This means if something goes catastrophically wrong — a bad migration wipes a table, a rogue query deletes rows — you can restore to yesterday's state.
But daily backups have a gap: anything that happened today is not backed up yet. For critical operations (major schema changes, bulk data modifications), make a manual backup first. Export the affected table to a CSV, or use Supabase's point-in-time recovery if your plan supports it.
The rule of thumb: if a mistake would take more than an hour to fix manually, back up before you start.
Your first design will be wrong. That is expected. The database grows with your product.