Next.js PCI Compliance
If you’re building e-commerce applications with Next.js, your PCI compliance posture depends entirely on how you handle payment data. The good news: Next.js’s architecture makes it relatively straightforward to achieve SAQ A or SAQ A-EP compliance by keeping cardholder data away from your servers. The challenge: misconfiguration can accidentally expand your scope to SAQ D, bringing hundreds of additional requirements into play.
Technical Overview
Next.js operates as a full-stack React framework, supporting both server-side rendering (SSR) and static site generation (SSG). From a PCI perspective, this dual nature requires careful consideration — your client-side code might qualify for reduced scope, while server-side API routes could bring your entire infrastructure into scope if they touch payment data.
Architecture Considerations
The framework’s flexibility means you can architect for minimal PCI scope:
Client-side tokenization: Your React components communicate directly with payment processors like Stripe, Square, or Braintree, bypassing your Next.js server entirely. The browser sends card data straight to the payment provider’s PCI-compliant infrastructure, and your server only receives non-sensitive tokens.
API route isolation: Next.js API routes (`/pages/api` or `/app/api`) should never process raw card numbers. If you must handle payment operations server-side, use provider SDKs that work exclusively with tokens.
Edge runtime implications: Next.js Edge Functions run in isolated environments closer to users. While this improves performance, remember that any function handling payment data expands your compliance scope to include that edge infrastructure.
Defense-in-Depth Positioning
In the PCI compliance model, Next.js applications typically sit at the presentation layer, ideally outside the Cardholder Data Environment (CDE). Your defense strategy should focus on:
- Network segmentation between your Next.js application and any systems that might process payments
- Web application firewalls (WAF) protecting against common attack vectors
- Content Security Policy (CSP) headers preventing malicious script injection
- Subresource Integrity (SRI) ensuring third-party payment scripts haven’t been tampered with
PCI DSS Requirements Addressed
Your Next.js implementation directly impacts several PCI requirements:
Requirement 2: Default Passwords and Security Parameters
Next.js applications must follow secure configuration practices:
- Remove example API routes before production deployment
- Disable source maps in production builds
- Configure proper CORS headers for payment endpoints
- Implement rate limiting on API routes
Requirement 6: Secure Development
Next.js development practices align with several sub-requirements:
6.2 (Secure Systems and Applications): Your build process should include dependency scanning, static code analysis, and automated security testing. Tools like `npm audit`, Snyk, or GitHub Dependabot catch vulnerable dependencies before deployment.
6.4 (Change Control): Every deployment affecting payment flows needs documented approval. Your CI/CD pipeline should enforce code review requirements and maintain deployment history.
6.5 (Common Vulnerabilities): Next.js helps prevent many OWASP Top 10 vulnerabilities by default, but you must still address:
- XSS prevention through proper data sanitization
- CSRF protection on payment-related forms
- SQL injection prevention in API routes (use parameterized queries)
- Proper authentication and session management
Requirement 8: Unique User IDs
If your Next.js application includes admin interfaces for payment management:
- Implement multi-factor authentication (MFA) using NextAuth.js or similar
- Enforce role-based access control (RBAC) for payment-related functions
- Log all administrative actions affecting payment configuration
Requirement 11: Security Testing
Your Next.js application requires:
- Quarterly ASV scans of public-facing infrastructure
- Annual penetration testing if you’re SAQ D
- Continuous vulnerability scanning in your CI/CD pipeline
Implementation Guide
Step 1: Scope Reduction Architecture
Start by designing for minimal PCI scope:
“`javascript
// pages/checkout.js – Client-side tokenization example
import { loadStripe } from ‘@stripe/stripe-js’;
import { Elements, CardElement, useStripe, useElements } from ‘@stripe/react-stripe-js’;
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (event) => {
event.preventDefault();
// Card data never touches your server
const { token, error } = await stripe.createToken(elements.getElement(CardElement));
if (!error) {
// Send only the token to your backend
await fetch(‘/api/process-payment’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ token: token.id })
});
}
};
return (
);
}
“`
Step 2: Secure API Routes
Configure API routes to handle only tokens:
“`javascript
// pages/api/process-payment.js
import Stripe from ‘stripe’;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export default async function handler(req, res) {
// Validate request method
if (req.method !== ‘POST’) {
return res.status(405).json({ error: ‘Method not allowed’ });
}
// Rate limiting check (implement with redis or similar)
const rateLimitOk = await checkRateLimit(req);
if (!rateLimitOk) {
return res.status(429).json({ error: ‘Too many requests’ });
}
try {
// Process payment using token only – no raw card data
const charge = await stripe.charges.create({
amount: req.body.amount,
currency: ‘usd’,
source: req.body.token, // Token, not card number
description: ‘Purchase’
});
// Log for audit trail (no sensitive data)
await logTransaction({
transactionId: charge.id,
amount: charge.amount,
timestamp: new Date(),
userId: req.session.userId
});
res.status(200).json({ success: true });
} catch (error) {
// Log error without exposing details
console.error(‘Payment processing error:’, error.type);
res.status(400).json({ error: ‘Payment failed’ });
}
}
“`
Step 3: Security Headers Configuration
Implement security headers via `next.config.js`:
“`javascript
// next.config.js
module.exports = {
async headers() {
return [
{
source: ‘/:path*’,
headers: [
{
key: ‘X-Content-Type-Options’,
value: ‘nosniff’
},
{
key: ‘X-Frame-Options’,
value: ‘DENY’
},
{
key: ‘X-XSS-Protection’,
value: ‘1; mode=block’
},
{
key: ‘Referrer-Policy’,
value: ‘strict-origin-when-cross-origin’
},
{
key: ‘Content-Security-Policy’,
value: “default-src ‘self’; script-src ‘self’ https://js.stripe.com; frame-src https://js.stripe.com; connect-src ‘self’ https://api.stripe.com”
}
]
}
];
}
};
“`
Step 4: Environment-Specific Configuration
Separate development and production configurations:
“`javascript
// lib/config.js
const config = {
development: {
apiUrl: ‘http://localhost:3000’,
logLevel: ‘debug’,
enableSourceMaps: true
},
production: {
apiUrl: process.env.API_URL,
logLevel: ‘error’,
enableSourceMaps: false,
forceSSL: true
}
};
export default config[process.env.NODE_ENV || ‘development’];
“`
Testing and Validation
Compliance Verification Checklist
Before your QSA assessment, verify:
1. No cardholder data in logs: Review all console.log statements, error handlers, and debugging output
2. HTTPS enforcement: Test that HTTP requests redirect to HTTPS
3. Token-only processing: Audit API routes to ensure no raw card numbers
4. Security headers present: Use SecurityHeaders.com to verify implementation
5. Dependencies updated: Run `npm audit` and address all high/critical vulnerabilities
Automated Testing
Implement security tests in your CI/CD pipeline:
“`javascript
// __tests__/security.test.js
import { createMocks } from ‘node-mocks-http’;
import handler from ‘../pages/api/process-payment’;
test(‘API rejects requests with raw card numbers’, async () => {
const { req, res } = createMocks({
method: ‘POST’,
body: {
cardNumber: ‘4111111111111111’, // This should fail
cvv: ‘123’
}
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(res._getJSONData().error).toBe(‘Invalid request format’);
});
test(‘Security headers are present’, async () => {
const response = await fetch(‘https://yoursite.com’);
expect(response.headers.get(‘X-Content-Type-Options’)).toBe(‘nosniff’);
expect(response.headers.get(‘X-Frame-Options’)).toBe(‘DENY’);
});
“`
Evidence Collection
Maintain these artifacts for your compliance file:
- Network diagram showing payment flow
- API route inventory documenting which endpoints handle payment data
- Security scan results from your ASV
- Dependency audit reports
- Deployment logs showing change control
Operational Maintenance
Log Management
Configure structured logging for payment-related events:
“`javascript
// lib/logger.js
import winston from ‘winston’;
const logger = winston.createLogger({
level: ‘info’,
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: ‘payment-audit.log’ }),
]
});
export function logPaymentEvent(event) {
logger.info({
timestamp: new Date().toISOString(),
eventType: event.type,
userId: event.userId,
// Never log: cardNumber, cvv, or other sensitive data
transactionId: event.transactionId,
amount: event.amount
});
}
“`
Monitoring Requirements
Set up alerts for:
- Failed payment attempts exceeding threshold
- Unauthorized API access attempts
- Dependency vulnerabilities in production
- SSL certificate expiration
- Unusual traffic patterns on payment endpoints
Annual Review Tasks
Your yearly compliance maintenance includes:
- Reviewing and updating network diagrams
- Validating that all payment flows still follow approved patterns
- Updating security training for developers
- Reviewing and updating incident response procedures
- Confirming all third-party payment scripts are current
Troubleshooting
Common Implementation Issues
Mixed Content Warnings: Ensure all payment-related resources load over HTTPS. Check for hardcoded HTTP URLs in your codebase.
CORS Errors with Payment Providers: Configure proper CORS headers for payment provider domains:
“`javascript
// next.config.js
async headers() {
return [{
source: ‘/api/:path*’,
headers: [{
key: ‘Access-Control-Allow-Origin’,
value: ‘https://checkout.stripe.com’
}]
}];
}
“`
Token Expiration Handling: Implement proper error handling for expired payment tokens:
“`javascript
if (error.type === ‘invalid_request_error’ && error.code === ‘token_already_used’) {
// Prompt user to re-enter payment information
}
“`
Performance Impact from Security Measures: If security middleware impacts performance:
- Implement caching for static assets
- Use CDN for payment provider scripts
- Consider edge functions for geographically distributed users
FAQ
Does server-side rendering (SSR) automatically put my Next.js app in PCI scope?
SSR itself doesn’t determine PCI scope — what matters is whether your server processes cardholder data. If your SSR pages only handle tokens and never see raw card numbers, you can maintain SAQ A or A-EP compliance. The moment your server-side code touches actual card data, you’re in SAQ D territory.
Can I use Next.js API routes for payment processing and stay SAQ A compliant?
No, if your API routes process payments, you’re at minimum SAQ A-EP (if using only tokens) or SAQ D (if handling card data). True SAQ A compliance requires payment data to flow directly from the browser to the payment processor without touching your servers.
How do I handle PCI compliance for Next.js apps deployed on Vercel or other edge platforms?
Edge deployments don’t change your fundamental compliance requirements. If you’re processing payments through edge functions, that infrastructure becomes part of your CDE. Most merchants maintain reduced scope by ensuring edge functions only handle tokens, not raw card data.
What security scanning tools work best with Next.js applications?
Standard ASV scanning tools work fine for production deployments. For development, integrate tools like npm audit, Snyk, or GitHub Advanced Security into your pipeline. OWASP ZAP and Burp Suite handle dynamic testing of running applications effectively.
Should I implement my own payment forms or use embedded payment provider solutions?
From a PCI perspective, embedded solutions from established providers (Stripe Elements, PayPal Checkout, Square Web Payments SDK) significantly reduce your compliance burden. Building custom payment forms means taking responsibility for secure data handling, which dramatically expands your PCI scope.
Conclusion
Next.js provides a solid foundation for building PCI-compliant e-commerce applications, but the framework alone doesn’t guarantee compliance. Your architectural decisions — particularly around payment data handling — determine whether you face dozens or hundreds of security requirements. By implementing client-side tokenization, securing your API routes, and maintaining proper security controls, you can leverage Next.js’s capabilities while minimizing your PCI compliance scope.
The key is planning your payment architecture before writing code. Once cardholder data touches your servers, you can’t easily reduce scope without significant refactoring. Start with a minimal-scope design, implement security controls systematically, and maintain evidence for your annual assessment.
Ready to validate your Next.js payment architecture against PCI requirements? PCICompliance.com gives you everything you need to achieve and maintain PCI compliance — our free SAQ Wizard identifies exactly which questionnaire you need based on your payment flow, our ASV scanning service handles your quarterly vulnerability scans for production deployments, and our compliance dashboard tracks your progress year-round. Start with the free SAQ Wizard to understand your compliance requirements or talk to our compliance team about securing your Next.js application.