How to set up simple password protection in Next.js - without a database and UI?
- The easiest way is to enable Vercel password protection, but it requires a Pro plan 💵.
- Or with a free tier you can do it with Cloudflare, although it involves multiple steps (up to 12).
Or, you can implement it in just a few lines of code: without libraries, no database or third-party services.
Ingredients
To implement simple password protection in a Next.js app, we need following ingredients 🧪:
- WWW-Authenticate 🛡️ header with
Secure Area
value - prompts the browser to show a login dialog:
Copied!
return new Response('Auth Required', {
status: 401, // status 401 is required, won't work without it
headers: {'WWW-Authenticate': 'Basic realm="Secure Area"'}
})
- Next.js Middleware wraps specified routes with executable logic where we validate authentication:
Copied!
export const config = { matcher: ['/((?!_next).*)'] } // excludes static assets
export default function middleware() { ... }
- A basic auth check function, which decodes the base64 header and compares it to predefined credentials:
Copied!
export const isAuthorized = (authHeader: string | null): boolean => {
if (!authHeader?.startsWith('Basic ')) {
return false
}
const decoded = Buffer.from(authHeader.slice(6).trim(), 'base64').toString('utf8')
const [user, pass] = decoded.split(':')
return user === BASIC_USER && pass === BASIC_PASS // should be stored in env variables
}
Full Code
middleware.ts
Copied!
import {NextRequest, NextResponse} from 'next/server'
const BASIC_USER = process.env.BASIC_USER ?? ''
const BASIC_PASS = process.env.BASIC_PASS ?? ''
if (!BASIC_USER || !BASIC_PASS) {
throw new Error('Missing env.BASIC_USER or env.BASIC_PASS')
}
export const isAuthorized = (authHeader: string | null): boolean => {
if (!authHeader || !authHeader?.startsWith('Basic ')) {
return false
}
const decoded = Buffer.from(authHeader.slice(6).trim(), 'base64').toString('utf8')
const [user, pass] = decoded.split(':')
return user === BASIC_USER && pass === BASIC_PASS
}
export function middleware(req: NextRequest) {
const authHeader = req.headers.get('authorization')
if (isAuthorized(authHeader)) {
return NextResponse.next()
}
return new Response('Auth Required', {
status: 401,
headers: {'WWW-Authenticate': 'Basic realm="Secure Area"'}
})
}
export const config = {
matcher: '/((?!_next).*)'
}
When you visit a protected page, the browser will display a login prompt:

- Once the user submits valid credentials, the browser sends the appropriate
Authorization
header on each subsequent request, so that prompt not shown till header is passing auth check 🎉
⚠️
Middleware troubleshoot, in case it does not triggered:
- Ensure the file is placed in the correct directory:
app
router:./middleware.ts
in the root of the project or in./src/middleware.ts
in casesrc
structure used.page
router:./pages/_middleware.ts
or./src/pages/_middleware.ts
server.js
and other cases where middleware is not supported: logic should be also placed in middleware, for example: express middleware
Summary
This approach provides a lightweight password protection mechanism for Next.js apps:
- No user interface or login page needed.
- No database or third-party service involved.
- Easy/quick to implement and sufficient for internal tools or staging environments.
- Bonus 🌟: those pages won't be indexed/parsed by Google bots/crawlers, which is good option of keeping dev environment private.
⚠️
Keep in mind that this solution is vulnerable to brute-force attacks, and the credentials are not encrypted. It should only be used for staging or temporary environments.
- Code - https://github.com/ra2dev/ratu-dev-discussions/blob/main/examples/password-protect-nextjs-pages/middleware.ts
- Sample app - https://ratu-dev-discussions.vercel.app/ (user:
admin
/pass:admin-password
)