Stop 34 of 42 · The Rulebook · Insecure database schemas

A ledger that was built wrong

Before any teller serves a customer, someone designed the bank’s ledger: which columns exist and how they are stored. If that design writes passwords in plain ink and leaves out a column for who owns each row, no teller can follow the rules, however careful they are.

Severity Low–HighCategory VariousThe RulebookSources checked 25 Oct 2026

Watch it happen

Play it as

Mallory gets a copy of the ledger. Passwords and ID numbers are in plain ink, and no column says who owns each row.

Arrival

Mallory walks in. Before any teller serves anyone, someone designed the ledger: its design decides what can ever be protected.

1 / 5

In bank terms and app terms

In the bankIn your app
Plain ink columnsFields like password, api_key, secret or access_token stored as readable text
Scrambled or encrypted columnsHashed passwords, and encrypted email, phone or SSN where you can
Owner columnA user_id or tenant_id on every table, so policies and queries can enforce ownership
Ledger designYour Prisma schema or Drizzle table definitions

Are you exposed?

You might be exposed if…
  • Your schema has columns named password, secret, api_key or access_token with no hash or encrypted version.
  • You store email, phone, address or ID numbers in plain text, or card details as ordinary strings.
  • A multi-tenant app has orders, invoices or users tables with no tenant_id (or user_id) column.
What to do about it
  1. Hash passwords with a slow algorithm (bcrypt or argon2), encrypt other secrets at rest with keys kept outside the database, and never store raw tokens you can avoid keeping.
  2. Add an owner or tenant column (user_id, tenant_id) to every table that holds someone’s data, and filter on it.
  3. Keep roles in their own table rather than an isAdmin column on users, so a stray update can’t promote someone.

Track these in your to-do list →

What it looks like in code

// BAD: isAdmin on User model
model User {
  id      String  @id @default(uuid())
  email   String  @unique
  isAdmin Boolean @default(false)  // Mass assignment target!
}

// GOOD: Separate Role model
model User {
  id    String @id @default(uuid())
  email String @unique
  roles Role[]
}

model Role {
  id     String @id @default(uuid())
  name   String // "admin", "user", "moderator"
  userId String
  user   User   @relation(fields: [userId], references: [id])
}

Ask your AI tool to fix it

Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.

Prompt
Review my Prisma or Drizzle schema. List every column that holds a password, secret, API key, token, or personal data (email, phone, address, SSN) and recommend whether to hash or encrypt it. Add a user_id or tenant_id column to every table that stores user-owned data, with a foreign key, and move role or isAdmin fields into a separate roles table. Show the migration.

Where this comes from

The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.

Spot a mistake or something out of date? Report a correction · How these guides are made and checked