← Blog
·9 min read

Prisma + Supabase Security: The Risks Most Guides Skip

Raw queries, RLS bypass risks, connection string exposure, and migration safety in Prisma + Supabase apps. A practical security guide with code examples.

Rod

Founder & Developer

Prisma and Supabase are a popular combo. Prisma gives you type-safe queries and a clean migration workflow. Supabase gives you a managed Postgres database with auth, storage, and a generous free tier. Together they cover a lot of ground. But the prisma supabase security story has some gotchas that most tutorials gloss over — and a few of them can leave your data wide open.

This post covers what actually matters: RLS bypass, raw query injection risks, connection string exposure, and running migrations safely. These are the patterns we see flagged in real scans at Data Hogo.


The RLS Bypass Problem With Prisma

This is the most important thing to understand, and most Prisma + Supabase tutorials don't mention it.

Supabase's Row Level Security (RLS) works by applying policies at the Postgres role level. When a client calls Supabase through the official client library, it connects as the anon or authenticated role — and RLS policies run. When you connect Prisma directly to Postgres using the full connection string (which uses the postgres superuser or the service_role equivalent), you connect as a superuser. RLS does not apply.

That means Prisma can read, update, or delete any row in any table — regardless of what your RLS policies say.

This isn't a bug in Prisma or Supabase. It's how Postgres roles work. But it catches a lot of developers off guard, especially if they've set up careful RLS policies and assume everything is protected.

The practical consequence: If your Prisma queries run server-side and you control the logic, you're okay as long as you apply the same filtering yourself. But if your connection string ever leaks, or if a server-side API route has an authorization flaw, there's no RLS safety net catching it at the database level.

The full RLS setup guide for Supabase is in the Supabase RLS security checklist — that's where you want to be if you're not using Prisma as your only query layer.


Prisma Raw Queries and SQL Injection

Prisma's generated query methods (findMany, create, update, etc.) use parameterized queries under the hood. You can't inject SQL through those. The risk comes when you reach for $queryRaw or $executeRaw.

The unsafe pattern

// BAD: user input interpolated directly into SQL string
const userId = req.query.id; // attacker controls this
const result = await prisma.$queryRaw(
  `SELECT * FROM users WHERE id = '${userId}'`
);

An attacker can send ' OR '1'='1 as userId and get every row in your users table.

The safe pattern

// GOOD: use the tagged template literal version of $queryRaw
const userId = req.query.id;
const result = await prisma.$queryRaw`
  SELECT * FROM users WHERE id = ${userId}
`;

The tagged template version automatically parameterizes your inputs. The Prisma runtime sends SELECT * FROM users WHERE id = $1 with userId as a separate bound parameter. The SQL and the data never mix.

The same rule applies to $executeRaw. If you're writing raw SQL anywhere, always use the tagged template form.


Connection String Security: The Two Supabase URLs

Supabase gives you two database connection strings, and using the wrong one in the wrong context creates problems.

Direct connectionpostgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres

This connects to Postgres directly. It's what you use for prisma migrate deploy. It doesn't go through PgBouncer, so long-lived connections work fine. Don't use this in serverless functions — each function invocation opens a new connection and you'll hit Postgres connection limits fast.

Pooler connectionpostgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres

This goes through PgBouncer. Use this for your application queries in serverless environments. Add ?pgbouncer=true to your DATABASE_URL so Prisma knows not to use prepared statements (PgBouncer in transaction mode doesn't support them).

In practice, your schema.prisma setup should look like this:

// GOOD: separate URLs for migrations vs queries
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")      // pooler URL — for queries
  directUrl = env("DIRECT_URL")        // direct URL — for migrations
}

Both variables live in your .env file and should never appear in your codebase. The directUrl field was added specifically so you can keep serverless-safe connection handling for queries while still running migrations correctly.

Never log your DATABASE_URL. If you're debugging connection issues, log the fact that a connection failed — not the URL itself. A logged connection string containing your Supabase password is just a credential leak with extra steps.


Protecting Your Connection String From Leaking

The most common way Prisma connection strings leak in repos we've scanned:

  1. .env committed to Git (either accidentally or because .gitignore was missing)
  2. DATABASE_URL logged in a catch block during a failed migration
  3. CI/CD config files that hardcode secrets for "convenience"
  4. next.config.ts that accidentally exposes env vars to the client bundle

On point 4 — in Next.js, any env var prefixed with NEXT_PUBLIC_ gets embedded in the client-side JavaScript bundle. Your DATABASE_URL should never have that prefix. But even without it, verify by checking your browser's Network tab — if your connection string appears in any response payload or script tag, something is wrong.

Run grep -r "DATABASE_URL" . in your repo and look at every match. It should only appear in .env, .env.example (with no actual value), and configuration files that reference process.env.DATABASE_URL. Nowhere else.

If you think your connection string may have been exposed, reset the database password in Supabase's dashboard under Project Settings > Database. This generates a new password and invalidates the old one.


Migration Safety With Prisma and Supabase

Prisma migrations run as the Postgres superuser. That means they can drop tables, alter columns, and modify constraints without any safety net. A few rules that matter in production.

Never run migrations manually against production. Run them through your deployment pipeline — GitHub Actions, Vercel deploy hooks, Railway release commands. This gives you a review gate before anything runs.

Separate destructive migrations from code deploys. If you're dropping a column that your current code still references, you'll break production during the deploy window. The pattern:

  1. Deploy code that stops using the old column (reads from new, writes to both)
  2. Verify the deploy is stable
  3. Second migration: drop the old column

Add --skip-generate in production migrations. Running prisma generate in a production migration step regenerates your Prisma client, which you don't want to do mid-deploy. Pre-generate it during your build step.

# BAD: generates client during migration (wrong place)
prisma migrate deploy && prisma generate
 
# GOOD: generate client at build time, only migrate at deploy
# In package.json "build" script:
# "build": "prisma generate && next build"
# In deploy step:
# prisma migrate deploy

Review the migration SQL before running it. Prisma generates .sql files in prisma/migrations/. Read them before committing. A migration that says DROP TABLE users should be obvious in review — but only if you look.


Schema Introspection and Information Exposure

Supabase exposes a PostgREST API on top of your database. By default, the anon role can introspect your schema through the API. That means an unauthenticated caller can discover your table names, column names, and data types by hitting https://[project].supabase.co/rest/v1/.

This isn't a critical vulnerability on its own, but it helps attackers understand your data model. Disable schema introspection for public access if your API isn't meant to be public:

Go to Supabase Dashboard > API Settings > "Expose tables and views in API" — and either restrict which tables appear, or disable public introspection entirely for sensitive tables.

At the Prisma level, don't expose your database schema structure through error messages in API responses. A Prisma PrismaClientKnownRequestError printed raw in a 500 response tells an attacker your column names, constraint names, and sometimes your table structure.

// BAD: exposes Prisma internals to the caller
} catch (error) {
  return Response.json({ error: error.message }, { status: 500 });
}
 
// GOOD: log internally, return a generic message
} catch (error) {
  console.error("Database error:", error);
  return Response.json({ error: "Something went wrong" }, { status: 500 });
}

Check Your Repo for These Issues

The patterns in this post — exposed connection strings, raw query injection, service role key misuse — are exactly what automated scanners flag. We run these checks on every repo that connects to Data Hogo.

If you've built something with Prisma and Supabase and you're not sure whether any of these patterns are in your codebase, a scan will tell you in a few minutes.

Scan your repo free →

The free plan covers 3 scans per month. No credit card. If you want to see what a Prisma-specific finding looks like before committing, the security scanner page has example outputs.


Frequently Asked Questions

Does Prisma bypass Supabase Row Level Security?

Yes, if you use Prisma with the service role key or the superuser connection string, your queries bypass RLS entirely. Prisma connects at the database level and doesn't go through Supabase's API layer where RLS is enforced. You need to either use the anon/user role connection or manually apply RLS-equivalent filtering in your queries.

Is Prisma vulnerable to SQL injection?

Prisma's generated queries use parameterized statements, which are safe from SQL injection. The risk comes from raw queries using Prisma.$queryRaw or Prisma.$executeRaw. If you interpolate user input into those using template literals instead of the built-in $queryRaw tagged template, you introduce SQL injection vulnerabilities.

How should I store my Prisma DATABASE_URL for Supabase?

Store it as an environment variable only — never hardcode it. Never commit your .env file. Never log the DATABASE_URL anywhere, including in error handlers. Rotate it immediately in Supabase's project settings if you suspect it was exposed.

What connection string should Prisma use with Supabase?

For serverless environments (Vercel, Netlify), use the Supabase pooler connection string on port 6543 with ?pgbouncer=true appended. For migrations (prisma migrate deploy), use the direct connection on port 5432. Set both as separate env vars and use Prisma's directUrl field in your datasource block.

How do I run Prisma migrations safely on Supabase?

Use the direct database connection for migrations, not the pooler. Run migrations in your CI/CD pipeline rather than locally against production. Always back up data before running migrations on a populated database. Split destructive changes (drop column) from code changes into separate deploys to avoid breaking production during the deploy window.

prismasupabasesecuritysql-injectionrls