Every API route is authenticated and authorized by default.
Middleware validates JWT + role on every API call before your handler runs. Routes are protected by default, not by choice. You opt routes OUT of protection, not in.
The createSlimMiddleware function wraps every route (pages and API) with authentication and authorization checks. The only routes that bypass this are the ones you explicitly list in publicRoutes.
For API routes (anything under /api/), the middleware extracts the JWT from the Authorization header or session cookie, validates it against the IDP's JWKS endpoint, and checks the user's roles against the page_permissions table.
If the JWT is invalid, expired, or missing — 401. If the user's roles don't match the route's required roles — 403. Your handler never runs for unauthorized requests.
For additional logging, wrap your handlers with withApiLogging() to get structured request/response logs in the admin dashboard automatically.
This is the most important design decision in the MVP's security model. Routes are protected unless you explicitly mark them as public. In most frameworks, you add auth checks to routes that need protection. This means every new route is unprotected until someone remembers to add the check. That's backwards. The MVP inverts it: every route is locked down. You maintain a list of public routes (landing page, pricing, docs). Everything else requires authentication. New routes are automatically protected.
// middleware.ts — the security boundary
import { createSlimMiddleware } from '@payez/next-mvp';
export default createSlimMiddleware({
// ONLY these routes are public
publicRoutes: [
'/',
'/about',
'/pricing',
'/docs',
'/api/health',
],
// Everything else: auth required
// /api/*, /dashboard/*, /admin/* — all protected
});The middleware validates JWTs using the IDP's JWKS (JSON Web Key Set) endpoint. Keys are cached and rotated automatically — when CryptAply rotates your signing keys, the middleware picks up the new keys on the next validation cycle. Validation checks: signature (RSA/ECDSA), expiration (exp claim), audience (aud matches your client ID), issuer (iss matches your IDP URL). All standard OIDC validation — nothing custom or proprietary.
// What the middleware does on every request
// (you never write this — it's built in)
// 1. Extract token
const token = req.headers.get('Authorization')
?.replace('Bearer ', '')
|| session.accessToken;
// 2. Validate against IDP's JWKS
const payload = await verifyJwt(token, {
jwksUri: 'https://idp.payez.net/.well-known/jwks',
audience: process.env.VIBE_CLIENT_ID,
issuer: 'https://idp.payez.net',
});
// 3. Check roles against page_permissions
const required = getRequiredRoles(req.pathname);
if (!hasAnyRole(payload.roles, required)) {
return new Response(null, { status: 403 });
}
// 4. Attach user context and continue
// Your handler gets: req.user with id, email, rolesFor API routes where you want structured logging in the admin dashboard, wrap your handler with withApiLogging(). It logs: request method, path, status code, response time, user ID, and any error details. This is optional — the auth protection works without it. But if you want your custom API routes to show up in the admin Logs tab, this is how.
import { withApiLogging } from '@payez/next-mvp';
// Your API route with automatic logging
export const POST = withApiLogging(
async (req: Request) => {
const body = await req.json();
// ... your logic ...
return Response.json({ success: true });
}
);
// Now this route appears in the admin Logs tab:
// POST /api/orders 200 45ms user:42The middleware also sets security headers on every response: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and a Content-Security-Policy baseline. CORS is configured per-tenant. These headers protect against common web vulnerabilities (clickjacking, MIME sniffing, downgrade attacks) without any configuration. You can override them in next.config.ts if needed.
API Route Protection ships with every @payez/next-mvp install. 10K MAU free. No credit card required.