~/VibeHandbook
$39

Chapter 17 · 02

Case Study 2: A Small SaaS Tool with Auth + Database

The idea

A freelance designer wanted a private dashboard to log billable hours per client and export a monthly CSV (Comma-Separated Values — a plain text spreadsheet file any spreadsheet app can open). Nothing fancy, but it needed accounts (so her data was hers alone) and persistence.

The spec

Auth and a raise the stakes, so the spec got more specific about boundaries:

A logged-in web app where a user can: sign up / log in with email,
create clients, log time entries (date, client, hours, note),
see a table of entries filtered by month, and download that month
as CSV. Each user only ever sees their own data. Mobile-friendly.

"Each user only ever sees their own data" looks like a UX line. It is actually the security model of the entire app, compressed into nine words. Naming it in the spec meant we could point back to it every time the AI drifted.

The stack

For a solo builder, the winning move is a stack where auth and database are managed services, not code you write. We chose a Next.js app deployed on a host, with a hosted Postgres database and a drop-in auth provider that handles email login, sessions, and password resets for us. Less code to get wrong is less code an AI can get wrong on our behalf. Auth in particular is a category you almost never want to hand-roll with an AI: the failure modes are silent, the blast radius is everyone's account, and a managed provider has had its cases beaten on by millions of logins.

The managed pieces sit on the outside; the one rule you own sits in the middle — every query filters by user_id, so one user can never read another's rows:

 ┌──────┐          ┌─────────────┐         ┌──────────────────┐
 │ USER │─ login ─▶│ AUTH        │─ user ─▶│  NEXT.JS APP     │
 └──────┘          │ (managed)   │  id     │                  │
                   └─────────────┘         │  every query:    │
                                           │  WHERE user_id=? ─┼──┐
                                           └──────────────────┘  │
                                                                 ▼
                                                       ┌──────────────────┐
                                                       │ POSTGRES (managed)│
                                                       │ clients · entries │
                                                       └──────────────────┘
            ↑ managed = less to get wrong   ↑ the one boundary YOU enforce

The key prompts

We let the auth provider's own template do the heavy lifting, then directed the AI to layer the logic on top:

We're using [auth provider]'s Next.js starter. Add a Postgres
schema with two tables: clients (id, user_id, name) and
time_entries (id, user_id, client_id, date, hours, note). Every
query MUST filter by the logged-in user's id from the session.
Generate the migration and the typed data-access functions.

The "MUST filter by user_id" line was the most important sentence in the whole project. We repeated that constraint in nearly every that touched data, because the single scariest bug in a multi-user app is one user seeing another's rows. Repetition feels redundant when you're typing it; it is exactly the redundancy that saves you, because the model has no memory of how much that constraint matters across separate prompts.

For the export:

Add a /api/export route that takes a month (YYYY-MM), pulls the
logged-in user's time_entries for that month joined to client
names, and streams a CSV download. Reject the request if there's
no valid session.

The obstacle

In testing, we created two accounts and discovered account B could see account A's clients in a dropdown. This is exactly the bug we feared. Rather than ask the AI to "fix it," we made it prove the problem first:

Account B is seeing Account A's clients. Show me every database
query in the codebase that reads the clients table, and for each
one tell me whether it filters by the session user_id. Don't fix
anything yet — just audit.

The audit surfaced one query — the dropdown loader — that had been written before we added the constraint and slipped through. We had it add the missing filter, then asked for a guard:

Add a single helper that every read goes through, which takes the
session and injects the user_id filter, so no future query can
forget it. Refactor the existing queries to use it.

That turned a one-off fix into a structural guarantee. Then we made the guarantee testable, because a guard you can't verify is just a hope:

Write a test that creates two users, has each create a client,
then asserts that user A's session can never read user B's client
through any of the data-access functions.

The lesson: when an AI introduces a security bug, don't just patch the instance — direct it to remove the category of mistake, then lock the category shut with a test that fails loudly if anyone reopens it.

The launch

We seeded a test month of data, exported the CSV, opened it in a spreadsheet to confirm the numbers and encoding were right, then set a strong database password and rotated the credentials out of any local file. We deployed to the serverless host, added the production environment variables in its dashboard, and gave her the . She onboarded herself with a real signup. The whole build was a weekend.

Want it offline?

Get the PDF + EPUB + downloadable prompt library + version updates.

$ Get the PDF — $39