# Software Eng for Vibe Coders: Databases + Storage

> Source: <https://read.technically.dev/p/software-eng-for-vibe-coders-databases>
> Published: 2026-08-25 12:45:49+00:00

Lots of you are probably building apps with AI, and we want to do our part to make sure those apps can thrive when they make contact with real users.

This 6-part series, **Software Engineering for Vibe Coders**, covers Art Vandelay’s sisyphean journey to ship an app to manage Vandelay Industries’ potato chip import & export business.

Thanks to our friends at [Railway](https://railway.com/?utm_medium=sponsor&utm_campaign=technically), who help you peacefully deploy those vibe coded apps, for sponsoring this series. Spin up anything your vibe coded app might need at [railway.com/new](https://railway.com/new?utm_medium=sponsor&utm_campaign=technically).

ICYMI, check out [Part 1 here](https://technically.dev/posts/software-eng-vibe-coders-frontends-backends).

## The TL;DR

Your app remembers things by storing them in a ** database**.

A

**database** is like a spreadsheet (has tables + rows containing data), except with more rules on how to interact with it.The database

is the rulebook: what every column can hold, and how tables connect to each other.[schema](https://technically.dev/universe/schema)Changing the schema on a live app is a

, which must be done with care.[migration](https://technically.dev/universe/migration)Big files (images, PDFs) don’t go in the database. They go into

**object storage**(like[AWS](https://technically.dev/companies/aws)S3).

Your coding agent likely created (**provisioned)** a database for you, which is great. In the old days prior to AI, you used to have to visit a database vendor down by the old port and perform a series of ritual incantations to get a database up and running. Today, it’s just a few clicks.

Databases aren’t glamorous, but they’re the most likely piece of the stack to ruin your big launch, so you’ll want to know how to properly care and feed them.

## Previously, at Vandelay Industries

Let’s recap quickly what we’re building in this series…

**Art Vandelay** - proprietor of Vandelay Industries, importer and exporter of fine potato chips - vibe coded an app: a public app where prospective suppliers can do business with Vandelay. Art is very proud of it.

In [Part 1](https://technically.dev/posts/software-eng-vibe-coders-frontends-backends) we split that app into its two halves: the ** frontend** (everything suppliers see) and the

**(everything they don’t), talking to each other through an**

[backend](https://technically.dev/universe/backend)[API](https://technically.dev/universe/api). Understanding the difference between these two foundational app building blocks is critical if you want to vibe code apps that actually work.

To work with Mr. Vandelay you must submit an application (he’s very selective). Here is a table of all of the pending applications that Art is going to run through and evaluate:

But where do these applications actually *live*?

They survive Art closing his laptop. They’ll need to survive a thousand suppliers applying at once, if this potato chip thing takes off the way Art assumes it will.

That somewhere is a database.

Your app has one too, if you’ve ever built anything with a login or saved list. Most coding agents at this point integrate with database providers (like Supabase, Railway, Neon, etc) and will set one up for you.

## What a database actually is

A database is a [spreadsheet that grew up](https://technically.dev/posts/the-beginners-guide-to-databases). Open a database and you see a shape you already know - tables, rows, columns:

If you’ve used Excel, nothing here should scare you.

The difference is *who’s reading it*. A spreadsheet is read by a person, who can glance at a typo or missing data, shrug, and (try to) fix it.

A database is read by code, which cannot shrug (that we know of). So the database is stricter: every column has one fixed job (e.g. to hold country names), and it holds the line on that job no matter what. If you try to put a phone number in that country name column, the database will tell you to pound sand. This is a good thing, because it means apps reading from the database know exactly what to expect.

So the entire stability, the very fate, of your vibe coded app depends on clearly defining your table structure (like you would in spreadsheet tabs).

There’s nuances to this, that start to matter when your app picks up more users, like:

How tables connect together (using something called

**foreign keys**). Part of the whole shtick of a database is storing data*efficiently*through the use of multiple, interconnected tables.How tables should be optimally queried (called

**indexes**). As tables get very big with much data, you start to need to worry about how fast you can get the data you need out of them.

When your app experiences runaway success (like a freight train, perhaps), you’ll want to know how to wield these tools, so that your app doesn’t “fall over” and leave your users holding the bag.

## Why coding agents pick Postgres (and what Railway, Supabase, and Neon actually are)

When your coding agent built your backend, it made a decision you might’ve missed: it reached for a ** relational database**. Relational databases are the default choice for the kind of everyday data a

[web app](https://technically.dev/universe/web-app)runs on, and the two big ones are

[Postgres](https://technically.dev/posts/whats-a-relational-database)[and](https://technically.dev/posts/whats-a-relational-database)

**. Postgres seems to be most popular with coding agents.**

[MySQL](https://technically.dev/posts/whats-a-relational-database)But “Postgres” is just the engine. It has to run somewhere, and *that’s* the decision your coding agent actually made on your behalf. There are many options, and they’re somewhat different:

: all-in-one Postgres, object storage to store stuff like photos, and app deployment to publish it to the internet.[Railway](https://railway.com/?utm_medium=sponsor&utm_campaign=technically): managed Postgres that also includes[Supabase](https://supabase.com)[authentication](https://technically.dev/universe/authentication)(so users can sign up) + object storage. You deploy your app + backend separately (typically on a[cloud](https://technically.dev/universe/cloud)service like[Vercel](https://technically.dev/companies/vercel)).: similar to Supabase, except it’s “[Neon](https://neon.com)[serverless](https://technically.dev/universe/serverless),” meaning it scales up and down with usage (rather than having a set “database server” that’s yours).

They’re all the open-source Postgres database engine underneath; what you’re choosing is who runs it and what else comes in the box.

The [database market](https://technically.dev/posts/the-details-production-databases) is having a proper moment due to vibe coding, with providers reporting that a big chunk of their new growth comes from AI agents spinning up databases exactly like yours.

Today there are hundreds (maybe more) of database providers. Outside of the user friendly, easy to get-started-with-a-credit-card homies like the list above, you have big behemoths like Oracle, [MongoDB](https://technically.dev/companies/mongodb), AWS, etc. But because vibe coding with agents tends to prioritize what’s easiest to set up today, you’ll end up with the above most of the time. If you become a massive business like JetBlue with millions of transactions per second…you might get a call from the Oracle sales team.

## Schemas and relations: the rulebook

The database’s strictness – which data is allowed in which columns – has to be written down somewhere, and that somewhere is the ** schema**: every table, every column, what type of data each column accepts, and how the tables connect to each other.

Why does Art care? Because his code is counting on those rules.

For example, suppliers in the applications need to report their estimated monthly volume in kilograms (get on the metric system boys). The est_monthly_kg column holds a number, and the dashboard in the app displays that number.

If there was a word (like Newman) instead of a number stored in that column, it’d break the app at some level, since the app is built to assume it’s getting a number there. The “blast radius” of that error would depend on how well your code guarded against data type errors, but someone somewhere would be unhappy. In the worst case, which happens all the time to real companies, the error can break the entire app.

The schema is what prevents that: declare the column a number, and the database turns away anything that isn’t one. It’s a bouncer with a clipboard, and only 10s are allowed into the club (nailed it).

The schema is also why databases *split* data across tables instead of piling everything into one giant sheet: to store important data only once, so that it’s easy to update. This concept, often called Normalization, merits a little more explanation.

Let’s say Kostanza Logistics applies twice to be a supplier (of course they do). If their country (“Latvia”) was stored individually on every application, then if they updated their country they’d have to hunt it down on every application as well.

So instead, you split it in the schema: a suppliers table holds each company exactly once, and the applications table just points back to it with a supplier_id:

One supplier, many applications. This is called a **relation**, and it’s the reason these are called [relational databases](https://technically.dev/posts/whats-a-relational-database). Hopefully these relations get along better than my family relations.

**🚨 Confusion Alert**

You’ll also hear about [NoSQL](https://technically.dev/universe/nosql) databases, which trade this strict structure for flexibility and easier scaling. They’re real, they’re widely used, and you almost certainly don’t need one — plain relational Postgres is more than enough for nearly anything you’ll build. Picking NoSQL because you’re picturing viral scale is giving yourself a hard problem you haven’t earned.

The art (sorry) and science of designing these relations thoughtfully as part of the discipline of database administration.

## What breaks when the suppliers show up

Everything above is enough to *understand* your database. But we want to keep it running when it goes viral and Art is drowning in millions of applications.

The problem with databases (like many things in apps) is that everything works perfectly when you’re testing solo on your laptop, or with a few friends at the local diner:

Issues appear at the worst time: when you actually start getting users, and they start doing things you didn’t plan for. Let’s game out a few scenarios:

**Two people, one pallet.** Vandelay is down to its last pallet of Costanza’s Sea Salt, and two orders land on it a millisecond apart. What stops the app from selling the same pallet twice?

In this case it’s a bundle of guarantees that goes by the acronym ** ACID**. The letters stand for things, but the core promise is: the database processes actions

**in order, without letting them collide**, even when they arrive in the same instant. The underlying Postgres engine is built specifically to handle situations like this.

It locks the row, fills one order, and politely tells the second it’s too late. With one user this literally cannot come up.

**Connections run out.** Postgres can only hold a couple hundred open connections at once. That’s fine, until your app is deployed as serverless functions that each grab their own connection, traffic spikes, and thousands of them are fighting over those slots.

Suddenly the app is refusing logins with “too many connections,” which is *the* classic faceplant when a vibe coded app meets really traffic. The fix is a **connection pooler**, which funnels many app connections through a few real ones (Supabase, Neon, and Railway all offer one). You don’t need to build it, but you need to know to turn it on.

**The page that was fast at 100 rows dies at a million.** Without an **index**, the database finds the right row(s) to match a [query](https://technically.dev/universe/query) by checking *every* row. Not a big deal with limited users, but agony as you scale. An index is a shortcut that you add on the columns you actually search and join by, like supplier_id.

This is the single most common reason an app is snappy in the demo and unusable the week it catches on.

**🖇 Workplace Example**

Ask any engineer about the time a page “suddenly got slow,” and you’ll hear the same story: it was never slow — it just never had enough data to be slow, until one day it did. The fix is almost always an index that had been missing since launch. Your AI won’t add every index you need. Knowing to ask your coding agents to review your indices (indexes?) is important.

**Know where to find your database backups.** The managed providers (Railway et al) typically take automatic backups and can roll your database back to a moment in time (or at least the prior day, depending on how much you pay them).

A backup is like a fire extinguisher. You’ll need it rarely, but when you need it (like when your coding agent accidentally wipes your database), you really need it.

Check now plz, it takes just a minute.

## Storage: where the big stuff goes

One more thing your AI may have gotten quietly right or quietly wrong. Databases are great at structured data (names, numbers, dates). They are bad at storing big files like images and videos.

When a supplier attaches a photo of their chip facility to an application, that photo does not belong *in* the database; stuffing files in there makes every query slower and the whole thing pricier to run, and it gets worse with every user.

Instead files go in **object storage**, which you can think about as a giant pile. Picture a huge labeled locker room for images, PDFs, and video.

The famous one is Amazon S3; on Railway it’s called a **Bucket**.

The two systems work as a team: the file sits in storage, and the database keeps a *link* to it. The database is the index card that says “the photo is at this address.” Storage is the locker that actually holds the photo.

## Migrations: changing a live database without taking it down

No database schema ever remains unchanged, except for products acquired by Atlassian.

After he designed the initial app with an initial database schema, Art realizes he wants to add a phone number column to the suppliers table. Simple enough, but on a database that real users are actively hitting, that’s a delicate operation, because the app depends on the current shape and one change can take the whole thing down.

A ** migration** is the safe way to do this. Instead of reaching into the live database by hand, the change gets written down as a file (add_phone_number_to_suppliers) and applied with one command (as part of

[CD](https://technically.dev/posts/whats-cicd)when running a deployment).

Because it’s a file, the change is documented, reviewable before it runs, and reversible. It also gets committed alongside your code, so your project carries an ordered history of every structural change your database has ever had.

If you need to create a new version of your database for some reason, you literally just rerun these files in order.

It’s important to be skeptical of AI-generated migrations. Your coding agent will hand you a migration with total confidence, but “just adding a column” can easily turn into deleting an entire table if you’re not careful.

To get around this, you (and your coding agent) will want to test database changes on a local database running on your laptop, or in a dedicated staging database in the cloud. Don’t give your coding agent access to your [production database](https://technically.dev/universe/production-database) - that’s for you and your deployment process to touch.

## Where that leaves Art (and you)

The applications page from Part 1 is no longer a mystery: the data lives in a Postgres table on Railway, the schema keeps every column honest, relations keep suppliers from being stored twice, and ACID keeps two orders from buying the same pallet. Hopefully you now know what this all means.

You now know the dragons that’ll bite you eventually in production (managing schema changes, connection limits, indexes, backups), which means you can ask your coding agent about them *before* launch week instead of during it.

One problem left, and it’s a big one: that dashboard full of supplier data? Right now, anyone on the internet can open it. Art doesn’t know that yet. Newman does. That’s coming up in part 3, which will cover auth + security.
