Next.js + DrizzlePublic database connection URL

Next.js database URL exposed in the browser: what to do now

Treat a database connection string used through a `NEXT_PUBLIC_` variable in a Client Component as browser-visible. Stop direct browser database calls, move queries to a server route or server-only module, rotate the connection credential, and check what that credential could access. Changing the variable name alone does not remove a value from an already published JavaScript bundle.

This guide covers check 03: Secrets and key management in the Zenveus Production Readiness Standard.

For builders

What this means, in plain words

A database connection string is a credential, not a public setting. Next.js can put a NEXT_PUBLIC_ value into JavaScript that anyone can download. Rotate the exposed credential and move database queries behind server-side authorization.

A scoped repair request

Using an AI builder? Paste this

Use this prompt in Lovable, Cursor, Replit, or Claude Code with the relevant server files available.

Audit my Next.js client import graph for database credentials and NEXT_PUBLIC_DATABASE_URL. Move database access to server-only modules and authorized handlers, rotate exposed credentials, rebuild, and scan the served production bundle without printing secret values. Work on a branch with synthetic data and mocked external services. Show the smallest diff, identify required adapters and deployment settings, and add allowed and denied tests that prove side effects cannot happen before checks pass. Do not disable security checks to make a test pass.

The same failure may appear as

  • NEXT_PUBLIC database URL appears in browser code
  • Drizzle connection is imported by a client page
  • Database credential remains in a published bundle
  • UI login does not restrict direct database use

Find the failure layer

Run these checks before rewriting anything

Each check removes a class of causes. Keep the first failing result, its timestamp, and the production log beside it.

01

Client import graph

Trace imports into files marked `"use client"`.

If this failsA database helper enters the client graph; move it into a server-only module and expose only authorized server operations.
02

Published bundle

Confirm whether the credential was actually configured for that build.

If this failsA credential appears in served JavaScript; rotate it, deploy a clean build, and check cached artifacts.
03

Database permissions

Determine whether it can read or write user records, and whether row-level rules apply to the connection type.

If this failsOverbroad database permissions widen exposure; restrict the role and network access and review audit logs.

Ranked diagnosis

Common root causes, in the order we would test them

01

Credential uses a public prefix

NEXT_PUBLIC variables can be inlined into client JavaScript at build time.

02

Database helper enters the client graph

A Client Component imports code that initializes a direct database connection.

03

Old builds persist

Removing source references does not revoke credentials already delivered to browsers.

04

Server permission is missing

UI login cannot control direct use of a browser-visible connection string.

Step-by-step repair

How to fix this in your app

Edit the database helper (for example utils/db.ts), its Client Component imports, and the replacement server handler. Replace NEXT_PUBLIC_DATABASE_URL with a server-only variable.

Production safety ruleNever disable access controls, expose service keys, or add wildcard CORS as a routine shortcut.

01

Replace the database credential at the provider

Create a replacement credential with only the required privileges, switch the server to it, then revoke the exposed credential. Where exposure is active, contain access immediately. Review connection and query logs for unexpected use.

02

Mark the database module server-only

Add import server-only to the module that reads DATABASE_URL. Remove imports of that module from use client files. Never return the credential or a database object through props or API responses.

03

Move the query into an authorized server handler

Read identity from your verified session, then query only records that identity may access. Return a small response object rather than the raw database record.

04

Build and inspect a fresh deployment

Remove the old NEXT_PUBLIC variable, rebuild, and deploy. Search served JavaScript and source maps for the retired variable and credential locally without printing secret values. Clear stale caches, but rely on revocation to neutralize copies of old bundles.

Implementation example

Next.js: server-only database boundary

// lib/db.ts -- imported only by server code
import 'server-only';
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('DATABASE_URL is missing');
// Initialize your existing database driver with databaseUrl here.

// A Client Component calls an authorized HTTP route instead:
const response = await fetch('/api/my-content', { cache: 'no-store' });
if (!response.ok) throw new Error('Unable to load content');
const content = await response.json();

The server route must perform session and record authorization. Changing the environment-variable prefix does not revoke an old credential or remove already-distributed bundles.

Prove the repair

How to check that the fix worked

Run these checks with synthetic data in your test environment, then repeat the relevant acceptance checks after deployment.

  • Production build: no database helper imported into Client Components.
  • Fresh served assets: no connection credential present.
  • Retired credential: connection rejected by the database provider.
  • Replacement API: anonymous denied and user B cannot read user A records.

If the check still fails

If the credential remains visible, inspect transitive client imports, serialized props, source maps, and stale deployment assets. If direct database access still succeeds, confirm the exposed credential was revoked.

When the built-in AI fix makes it worse

Recover one reproducible failure.

Pause generated changes, restore a known working branch, and capture one failing request with its logs. Change one layer and rerun the allowed and denied checks before proceeding.

Engineering handoff

What Zenveus checks when the quick fix is not enough

We trace one production request through the complete path, isolate the failing boundary, and leave behind evidence your team can repeat.

Client graph

Public-prefixed configuration and database modules imported by client components.

Exposure scope

Served builds, old artifacts, database role grants, and access logs.

Server replacement

The new authorized data handler and owner or tenant constraints.

Clean-build proof

Credential absence in deployed JavaScript and failed use of the revoked secret.

Typical repair pattern

No database credential reaches the browser

The published bundle contains no connection secret and server routes deny unauthorized record access.

Evidence left behind
  • Root-cause note
  • Verified production check
  • Rollback and prevention steps

Before the next release

Prevent this failure from returning

Keep privileged environment values out of public prefixes and client imports.
Scan production bundles for secret names and credential patterns before release.

Clear answers

Questions teams ask before they touch production

01Is every `NEXT_PUBLIC_` variable unsafe?

No. Public identifiers are appropriate there. A credential-bearing database URL is different because it grants database access.

02Can I leave the URL and rely on UI login?

No. A browser-visible URL can be used outside your UI. Enforce access on the server and at the database.

03Does removing it from source revoke an old build?

No. Rotate the credential and replace published bundles as part of the fix.

04How do I know the fix worked in my app?

Run the verification checks on this page against your test environment, then repeat the relevant checks after deployment. Example code needs your app's authentication, data model, and configuration; reading the guide alone does not verify your deployment.

Official documentation and library references

Free next step

Check the boundary before you hand it over

The free tool helps you inspect this symptom. Its result does not establish whether the whole app is production ready.

The Verdict

Know whether the symptom is contained or structural.

We can see the symptom from here. What we cannot tell you from outside is whether it is contained or structural. A scanner collects evidence. A named senior engineer makes the decision. For $299, a named senior engineer reads your code and signs a written Verdict against the nine checks in the Zenveus Production Readiness Standard. The 48-hour clock begins when the required access and context are available. If the report does not give your developer a list they can act on, you do not pay.

The 48-hour clock starts when the required access and context are available.

Scroll to Top