[ 08 / 10 ] · Freshman Track

Lesson 08: Databases and Storage

16 minutes200 XP

Embark on your journey to building production grade apps.

Your app looks good and handles errors. But it can't remember anything, and it has nowhere to put the files your users want to upload. This lesson fixes both.


Right now, if a user types something into your app and refreshes the page, it's gone. If they try to attach a photo, there's nowhere for it to go. Your app has no memory and no filing cabinet.

A database gives your app memory. File storage gives your app somewhere to put the things that aren't neatly structured — images, audio, video, PDFs. Together, they turn a demo into a real product.

We're going to use Supabase for both. Supabase is a free service that gives you a database and file storage out of the box. One tool, two problems solved.

Security is a running theme in this lesson. Databases and storage are the first things attackers probe — that's where user data lives. Watch for the security callouts and don't skip them. Getting the basics right here is the difference between an app that leaks user data and one that doesn't.


What is a Database?

A database is where your app stores information. Think of it like a spreadsheet that your app can read from and write to.

Every app you use has a database behind it. Instagram stores your captions, likes, and followers in a database. Twitter stores your tweets. Spotify stores your playlists. When you close the app and reopen it, your stuff is still there. That's because it's saved in a database, not on your device.

Without a database, your app is like a whiteboard. It looks great while you're using it, but the moment someone erases it (or refreshes the page), everything is gone.

How databases are organized

A database is made up of tables. Each table stores one type of thing. If you're building a to-do app, you might have:

  • A users table (who's using the app)
  • A todos table (what they need to do)
  • A projects table (how they organize their tasks)

Each table has rows and columns, just like a spreadsheet. The todos table might look like this:

id  user_id  task              completed
--  -------  ----------------  ---------
1   abc123   Buy groceries     false
2   abc123   Walk the dog      true
3   def456   Finish homework   false

Each row is one item. Each column is one piece of information about that item. The id column gives each row a unique identifier so the database can find it.

What is a database?

What is a table in a database?


What is SQL?

A database holds your data. SQL (pronounced "sequel") is how you talk to it. It stands for Structured Query Language, and it's the language every major database on the planet speaks: Postgres, MySQL, SQLite, Supabase (which is Postgres under the hood), even Google's internal systems.

You'll see SQL a lot in this lesson. Your AI agent will hand you snippets and ask you to paste them into Supabase. Knowing what the snippets actually say turns "I'm blindly running stuff" into "I can read the commands before I run them." That's the difference between trusting your tools and being at their mercy.

SQL is surprisingly readable. Here are the four commands that cover 90% of what apps do:

  • SELECT reads rows. "Give me the tasks that belong to user abc123."

    SELECT * FROM todos WHERE user_id = 'abc123';
    
  • INSERT adds a new row. "Add a new todo called 'Buy groceries' for user abc123."

    INSERT INTO todos (user_id, task) VALUES ('abc123', 'Buy groceries');
    
  • UPDATE changes existing rows. "Mark todo #1 as completed."

    UPDATE todos SET completed = true WHERE id = 1;
    
  • DELETE removes rows. "Delete todo #2."

    DELETE FROM todos WHERE id = 2;
    

Your app hits the database thousands of times a day, and every one of those reads and writes is one of these four commands under the hood. Your AI agent is writing SQL for you. The job is to recognize it when you see it.

Why SQL Is Worth Knowing

Even if you never hand-write a query, here's why SQL pays off:

  • You can read what your agent wrote. Is this query safe? Does it filter by the right user? Is it deleting more than you intended? You can tell at a glance.
  • You can debug. When your app says "nothing's showing up," the fix is almost always in the query. Being able to open Supabase's SQL Editor and try the query yourself is a superpower.
  • It's permanent. SQL has been the standard for 50 years. Frameworks come and go. The SQL you learn today will be relevant in 2050.
  • It's portable. The same queries work on Postgres, MySQL, SQLite, and every major cloud database. Learn once, use everywhere.

You don't need to become an expert. Recognize the four commands above and you're set for this lesson and most of what follows.

What is SQL?

Why is it worth knowing SQL even if your AI agent writes queries for you?


What is File Storage?

Databases are great for structured data (text, numbers, dates, relationships). They are bad at raw files. A 5MB photo sitting in a database row is slow to read, slow to write, expensive to back up, and impossible to serve efficiently to a browser.

File storage is purpose-built for this. It's a cloud filing cabinet with a URL for every file. You upload a file, get back a path, save that path in your database, and serve the file directly from the cabinet when your app needs it.

Things that belong in storage:

  • Profile pictures and avatars
  • Images, videos, audio, PDFs uploaded by users
  • Exports, reports, generated files

The split is always the same: metadata goes in the database, bytes go in storage, the database points to the bytes.

Why use file storage instead of putting files directly into the database?


Let's Build a List App

We'll use the database by building the smallest thing that needs one: a list where items are added with an input field and deleted with a delete button. Refresh the page and the list is still there. That's what a database buys you.

Open your Repl's AI panel and paste:

I am building a project to help me learn databases. Today we will 
build a simple list where items are appended via an input field 
and deleted via a delete button.

Rules:
- The list MUST be stored in a real hosted database (Supabase, 
  Firebase, or similar). Do NOT use local React state, 
  localStorage, sessionStorage, or any client-only storage. 
  Do not use optimistic updates. The displayed list must always 
  come from the database.
- Every add inserts into the database and then re-reads the list 
  from it. Every delete removes from the database and then re-reads.
- Database credentials come from environment variables that are 
  ALREADY set in my shell as **`SUPABASE_URL`** and 
  **`SUPABASE_KEY`**. Read those exact names directly from 
  `process.env` at runtime. Do NOT ask me to create a `.env` / 
  `.env.local` / `config.js` / any other file with placeholders, 
  and do NOT ask me to re-export the variables under different 
  names (`VITE_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_URL`, etc.). 
  They're already set. Use them.
- Pick a stack that can actually read shell env vars at runtime. 
  Vanilla HTML/CSS/JS served by a tiny Node or Python local server 
  that reads `process.env` at request time is fine (and simplest). 
  Next.js server components or server routes are fine. **Do NOT 
  use Vite + React** for this — Vite inlines env vars at build 
  time, requires the `VITE_` prefix, and only reads them from an 
  `.env` file on disk, which breaks the "use my shell exports 
  directly" rule. If you use a framework with a prefix convention, 
  you're responsible for wiring `SUPABASE_URL` → whatever name 
  the framework needs, inside a `package.json` dev script — 
  without me having to do anything.
- Ask me zero follow-up questions about credentials — they're 
  already in my shell.
- If the database isn't configured yet (missing credentials or 
  missing table), show a clear error message in place of the list. 
  Do not silently fall back to a local-only list that pretends 
  to work.
- Keep the input field and the Add button always enabled and 
  clickable. The user should be able to type and click regardless 
  of the database state. If the database isn't configured, clicking 
  Add should simply attempt the request and show the error — don't 
  disable controls as a "guard."
- Keep the UI minimal. Standard system fonts, plain colors, no 
  gradients or fancy backgrounds. This is a learning exercise, 
  not a design showcase.

Run it locally so I can see it in my browser.

Replit Agent will scaffold the app, figure out it needs a database, and pop up an environment-variables panel asking for your Supabase Project URL and anon public key:

Replit Agent prompt asking for Supabase Project URL and anon public key, with an Environment Variables panel and a Save Variables button

You don't have those keys yet. Here's how to get them.

Create a Supabase Project

Supabase is a free database service. Go to supabase.com and sign up. You can use your GitHub account.

Supabase 'Get started' signup page with Continue with GitHub and email/password options

Once you're logged in, click New Project. Give it a name (like my-list-app), pick a region close to you, and create a strong database password. Save the password somewhere safe.

Supabase 'Create a new project' form with Organization, Project name, Database password, Region, and Security options

Click Create new project. It takes about 30 seconds to spin up.

Hit Connect

In your Supabase dashboard, click Connect at the top. A modal opens showing your keys:

Supabase 'Connect to your project' modal open to the .env.local tab, showing the Project URL and publishable key values

Copy the URL and the publishable key (the values to the right of each =) and paste them into Replit's env-vars panel. Then click Save Variables. Replit restarts the app and the agent wires it up.

Security — don't touch the second key. Supabase also shows a service_role key elsewhere. It bypasses every security rule in your project. If it leaks (in your frontend, in your Git history, in a log), anyone who finds it can read, write, or delete any data. Only use the anon / publishable key in your app. Never the service_role key. Bots scan public repos for keys within minutes.

Create the Table

Try adding an item in the preview. It probably won't work yet — you might see a Supabase error in the app, or the list might just stay empty after you click Add. Either way, the cause is the same: your agent wrote code that reads and writes a table, but the table doesn't exist in your Supabase project yet. Replit can't create tables for you — Supabase keeps schema changes out of what the agent can touch.

  1. Ask your agent to diagnose:

    > I tried adding an item but nothing happened (or I got an error 
      about a missing table). Can you diagnose what's wrong with 
      Supabase and give me the SQL I need to run to create the table 
      this app needs?
    
  2. The agent will reply with a CREATE TABLE ... statement. The table will look something like this:

    CREATE TABLE public.list_items (
      id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
      name TEXT NOT NULL,
      created_at TIMESTAMPTZ DEFAULT now()
    );
    
  3. In your Supabase dashboard, click SQL Editor in the left sidebar:

    Supabase sidebar showing Project Overview, Table Editor, and SQL Editor (highlighted)
  4. Paste the SQL the agent gave you and click Run.

  5. Back in your app, refresh the page (or click Refresh on the error box, if you got one).

Then toggle RLS off for the new table: Table Editor → [your table] → RLS disabled toggle at the top.

Security — you will turn RLS back on. Disabling it is fine while building solo. In the sophomore auth lesson you'll turn it back on with a policy scoped to each user. Never ship a real app with RLS disabled.

Try adding an item again. This time it should save.

Try It

Your preview should now show a working list. Type something, click Add — it appears. Click Delete — it's gone. Refresh the page — your items are still there.

That's a database.

What does the Supabase SDK do?


Adding Storage to Your App

Now let's let users upload files — an image attached to each list item, for example.

Create a Bucket

In Supabase Storage, a bucket is a named container for files (like a top-level folder). You'd typically have one bucket per kind of content: avatars, post-images, documents.

  1. In your Supabase dashboard, click Storage in the sidebar

  2. Click New bucket

    Supabase Storage empty state showing 'Create a file bucket — Store images, videos, documents, and any other file type.' with a green '+ New bucket' button
  3. Name it todo-attachments

  4. Leave Public bucket off for now

  5. Click Save

Security — public vs private buckets. A public bucket makes every file in it readable by anyone with the URL. Fine for things that are truly public, like profile pictures on a social app. A private bucket requires the owner's session to read — use this for anything users expect to stay private (receipts, medical docs, message attachments). Default to private. Turn public on only when you mean it.

Set a Storage Policy

Buckets use RLS-style policies just like tables. While you don't have authentication yet, set a permissive policy so uploads work, but plan to tighten it later:

  1. Click on your todo-attachments bucket

  2. Click the Policies tab

  3. Click New policyUse template → pick Allow access to JPG images in a public folder to anonymous users

  4. Under Allowed operation, check SELECT (read / download) and INSERT (upload). The list below shows which client functions each operation enables (upload, download, list, etc.)

  5. Click Save policy. Supabase shows a review screen with the exact SQL it'll run for each operation you picked:

    Supabase 'Reviewing policies to be created for todo-attachments' screen showing the INSERT and SELECT CREATE POLICY SQL statements, with 'Back to edit' and 'Save policy' buttons
  6. Click Save policy on the review screen to confirm. You'll land back on the Policies tab, which now lists your two new policies under todo-attachments. That's it — the bucket will now accept uploads and serve them back, as long as they match the policy's rules.

Heads up — what this template actually allows. Reading the SQL: only .jpg files, only inside a folder named public/, only for anonymous users. If your uploads use a different extension or path, the policy won't match and uploads will silently fail. Tell your agent to save images as public/{timestamp}.jpg so the policy matches (you'll do this in the upload step below).

Security — you'll tighten this later too. In the authentication lesson, you'll swap this out for a policy that restricts uploads and reads to each user's own folder ({user_id}/...). Right now, anyone with your app's URL can upload. Don't leave this open once you have real users.

Add a Column to Reference the File

You need somewhere in your table to store the path to each uploaded file. Ask your agent:

> I need to add a column to my list table in Supabase to store the 
  path to an uploaded image. What SQL should I run to add an 
  "attachment_path" text column that can be null?

The agent already knows what your table is called, so it'll give you the right statement. It'll look something like this:

ALTER TABLE public.list_items
  ADD COLUMN attachment_path TEXT;

In your Supabase dashboard, click SQL Editor in the left sidebar, paste what the agent gave you, and click Run (top-right of the editor).

Supabase SQL Editor with an 'alter table public.list_items add column attachment_path text;' query pasted and the Results pane showing 'Success. No rows returned' after clicking Run

You should see Success. No rows returned in the Results pane. Your table now has a spot for each uploaded file's path.

Upload Files From Your App

Tell your AI agent:

> add an optional file input to my add-item form. accept only JPG 
  images. when the user picks a file, show a small thumbnail or 
  the filename next to the input so they can see the file is 
  attached. the file should upload when they click Add (not when 
  they pick the file), so make sure the Add button is the obvious 
  way to submit both the text and the image together. upload to 
  the todo-attachments Supabase bucket under a path like 
  public/{timestamp}.jpg (the "public/" prefix and ".jpg" extension 
  are required by the storage policy). save the returned path into 
  the attachment_path column for the new item. if no file is 
  picked, just save the item without an attachment.

Display the Files

> when rendering each list item, if it has an attachment_path, use 
  supabase.storage.from('todo-attachments').getPublicUrl(path) to 
  build a URL. render images inline, and anything else as a download link.

Constrain What Users Can Upload

Storage is where a lot of security incidents happen. Without limits, a user can upload a 2GB file, a malicious executable, or an HTML file that runs scripts in other users' browsers. You need guardrails.

> before uploading, enforce these rules:
  - accept only image/jpeg, image/png, image/webp, and application/pdf
  - reject files larger than 5MB
  - rename uploaded files so the stored filename doesn't contain 
    user-supplied text
  - show a friendly error if the check fails

Security — why MIME and size checks matter. Browsers sometimes execute files they think are HTML or JavaScript, even if uploaded as images. An attacker who uploads a file named photo.jpg that is actually HTML can run scripts in other users' browsers — that's a classic upload-based XSS. Check MIME types on the client for UX, and rely on your bucket policy (and file size limits on Supabase) to enforce them server-side. Without a file size cap, one user can fill your quota or run up your bill.

Why validate MIME types and file size on uploads?


Security Recap

A quick mental checklist before moving on:

  • Keys: anon key in env vars; service_role key never leaves Supabase or a trusted server.
  • Database: RLS on in production, disabled only while testing solo.
  • Input validation: max lengths, trim whitespace, strip HTML before rendering back.
  • Buckets: default private; public only when content is truly public.
  • Uploads: MIME type and size caps on the client and enforced via bucket policies.
  • Never trust the client. Anything you enforce in the browser, also enforce in Supabase.

Database vs Blockchain: What Goes Where?

You've just learned how to store data in a database and files in cloud storage. Later in this course, you'll learn how to store data on a blockchain. They're not interchangeable — each is suited to different kinds of data.

Use a database (and storage) when:

  • Data is private or user-specific (a user's profile, their todos, their uploads)
  • Data changes frequently (likes, views, status updates)
  • You need to query it in complex ways (search, filters, sorting)
  • Performance and cost matter at scale — databases and storage are fast and cheap

Use a blockchain when:

  • Data needs to be public and verifiable by anyone
  • Data must be permanent and tamper-proof
  • No single entity should own or control it
  • You're dealing with value, ownership, or trust (tokens, contracts, votes)

A practical example: if you're building a marketplace, user profiles and product photos probably live in Supabase. But ownership of an item, the transaction history, and payment logic might live on a blockchain.

Most real-world apps use both. The database and storage handle the fast, private, everyday stuff. The blockchain handles the stuff that needs to be ownerless and permanent.

Which type of data is best suited for a blockchain?


What You Just Built

Your app now:

  • Remembers things with a Supabase database
  • Holds user-uploaded files with Supabase Storage
  • Keeps API keys out of reach of the agent and Git
  • Validates uploads so bad files can't get in
  • Runs with RLS as the firewall between your data and the internet

That's the real skeleton of a production app. It's missing one big piece: it doesn't know who is using it. In the sophomore track you'll add authentication, turn RLS back on, and make each user's data actually private.


What's Next

Your app has memory and file storage. Next, we'll explore Monad's architecture and how blockchain offers a fundamentally different approach to the problems we just solved: identity, data storage, and trust.

0/8 correct

0% — get all correct to complete

Sign Up to Track Progress