Prisma Schema Analyzer
A database schema is a statement about what the application stores. This analyzer reads every model block in the repository, goes through the fields one by one, and reports the ones that hold a secret, personal data or a payment ID without a protected twin, and the models that hold tenant data without a tenant column.
Field by field
The actor is the scanner alone. It parses the schema into models and fields, then runs four checks on each model. Watch which field lights up and which rule it satisfies.
1The scanner reads every file that contains model Name {, cuts the body with balanced braces and keeps only lines whose type is a Prisma scalar. A line with @relation is a virtual field and is skipped, so user never becomes a column.
2Check 1, secrets. The name contains an indicator such as access_token, and no field called <name>_encrypted, _hash or _hashed sits beside it. No twin, so High with confidence 0.90.
3Check 2, PII. email equals an indicator and has no encrypted twin. Medium, 0.80. A field that already ends in _encrypted, _hash or _hashed is skipped, so the twin is never flagged as PII.
4Check 3, tenant scope. It only runs when the schema has a model named like Tenant or Organization. UserSettings matches the scoped list and has no tenant_id. Medium, 0.75.
5Check 4, payment IDs. A provider prefix plus an ID suffix. Low, 0.70. Every finding carries the model, the field and the file, but no line number.
What clears a finding
Each finding names a missing twin or a missing column. Add it and the finding goes away, with one exception that the secret check makes visible.
1Start from the four findings of the walk. The left side is the model, the right side is what the analyzer reports for it.
2Add email_hash. The PII check looks for email plus a suffix in the same model, finds it, and the finding is cleared. The new field is not itself flagged.
3Add tenant_id. The tenant check returns early for a model that already has the column.
4Add access_token_encrypted. The secret check has no skip for names that are already encrypted, so the new field matches the same access_token indicator and is flagged in its place. The finding text recommends application-level encryption or a KMS.
5The payment ID has no twin rule. It stays at Low as a prompt to decide whether the identifier belongs in the threat model. Two findings are left.
Naming rules
The analyzer shares its word lists with the Drizzle analyzer. Drizzle checks the database column name, which is snake_case by habit. Prisma checks the field name as written, which is camelCase by habit.
1The four checks compare field names against lowercase snake_case lists. Prisma schemas are usually camelCase, so what is found depends on how a field is spelled. Four spellings, one rule each.
2tenantId is not tenant_id. A model that has the column under its camelCase name is still reported as missing it. This is a false positive.
3apiKey lowers to apikey, which does not contain api_key. A real secret field goes unreported.
4emailAddress lowers to emailaddress. The PII rule wants email exactly or an underscore boundary, so it is missed. email_address would be caught.
5password_hash contains password. The PII check skips hashed names, the secret check does not, so the hashed column is reported as a plaintext secret.
Why it matters
Schema design decides how bad a breach is. PII stored in plaintext becomes a liability when, not if, the database leaks, and a secret column means one leaked row is a working credential. A model without a tenant column has nothing to filter on, so a cross-tenant read is one forgotten where clause away.
GitHub (2012). A mass assignment bug let an attacker add his SSH key to the Rails core repository through a field that sat directly on the model. That specific pattern, an isAdmin or role field, is not one of the four checks the analyzer runs today.
Flagged vs clean schema
// BAD: four findings
model Organization { id String @id }
model UserSettings {
id String @id
email String @unique // PII, no email_hash
access_token String // secret in plaintext
stripe_customer_id String // payment ID (Low)
} // no tenant_id// GOOD: twin and scope fields present
model Organization { id String @id }
model UserSettings {
id String @id
tenant_id String
email String @unique
email_hash String
// access_token: keep it in a KMS or secrets store, not in this table
stripe_customer_id String // still Low: a judgement call
}The analyzer does not check isAdmin or role fields as mass assignment targets, missing createdAt and updatedAt, or a missing deletedAt. Its recommended fix, a separate Role model instead of an isAdmin column, is still sound schema design.
Limits and scope
| Behaviour | Detail |
|---|---|
| Input | Any indexed file that contains model Name {. No network, no credentials, no depth setting. |
| Fields it sees | Only scalar types: String, Int, BigInt, Float, Decimal, Boolean, DateTime, Json, Bytes. Enum-typed fields and relation lines are skipped. |
| Severity and confidence | Secret High 0.90, PII Medium 0.80, tenant scope Medium 0.75, payment ID Low 0.70. |
| Tenant check | Runs only when a model named Tenant, Organization, Org, Workspace or Company (or a lowercase plural) exists. It flags models whose lowercase name contains users, apps, subscriptions, purchases, orders, invoices, settings or profiles, so a singular Order is not examined. |
| Secret check | No skip for names that are already encrypted or hashed, so password_hash and access_token_encrypted are reported. |
| Name matching | Indicators are lowercase snake_case. camelCase spellings such as apiKey, emailAddress and tenantId do not match. |
| Not implemented | isAdmin and role fields, audit timestamp and soft delete checks. |