@payez/next-mvp
v4.5.0
Published
Drop-in authentication for Next.js. One package, zero secret management in production.
Readme
@payez/next-mvp
Drop-in authentication for Next.js. One package, zero secret management in production.
npm install @payez/next-mvp next-authInstalling is not finishing. There is no
postinstallstep and no generator, so the package cannot mount its own routes or pages into yourapp/directory, and it does not emit the CSS variables its components read. POST-INSTALL.md is the complete list of what you must still do by hand — including three that leave the site broken with nothing to tell you why: the auth routes, the login page, and the sixteen theme variables. Read it before you conclude something is wrong with the package.
How Secrets Work
You never manage signing keys or OAuth credentials directly. The package resolves them automatically at startup based on your environment.
| Environment | How it works | Secrets in your config? |
|-------------|-------------|------------------------|
| Dev | App calls IDP broker on your private network. One API key in .env.local. | One key (rotatable via CLI) |
| Production | App calls a cluster-internal endpoint. Network boundary = identity. | None |
| Enterprise | App uses an issued license key against your own or our IDP. | One key (rotatable via CLI) |
In production, there are no secrets to configure, rotate, or leak. The app proves its identity by being inside the cluster.
In dev, you have one key. Rotate it anytime:
npx @payez/cli secret rotateFull architecture: SECRET-MANAGEMENT-ARCHITECTURE.md
Features
- Zero-secret production deployment — no signing keys, no OAuth secrets in your env
- Complete authentication flow — login, logout, session management, password recovery
- Pre-built UI components — themed login, recovery, and verify-code pages
- Automatic token refresh — built-in refresh token handling with Redis session backing
- Google OAuth + 2FA — pre-configured providers, MFA with email/SMS
- Themeable — branding, colors, and layout via ThemeProvider
- Next.js 14/15 — App Router, React Server Components, middleware-ready
Installation
npm install @payez/next-mvp next-auth
# or
yarn add @payez/next-mvp next-authPeer Dependencies
Ensure you have these installed:
npm install next@^14.0.0 next-auth@^4.24.7 react@^18.2.0 react-dom@^18.2.0Development Scripts
PayEz MVP includes convenient development scripts for getting started quickly:
npm run dev:local
Local Development Mode - Perfect for first-time setup and testing without an external IDP:
- Auto-generates
NEXTAUTH_SECRETfor you - No external IDP connection required
- Great for UI development and testing
npm run dev:localThis will:
- Generate a secure random
NEXTAUTH_SECRET - Display the secret (save it to your
.env.local) - Start the Next.js dev server on port 3000
npm run dev:broker
Broker Mode - Production-like authentication with PayEz IDP:
- Uses OAuth/OIDC flow with PayEz IDP
- Client assertion authentication
- Required for testing real authentication flows
- This is a core feature for PayEz MVP users
npm run dev:brokerThis will:
- Enable broker mode (
USE_BROKER_MODE=true) - Set client ID for IDP authentication
- Start the Next.js dev server on port 3000
- Automatically kills any existing process on port 3000
Custom Port & Client ID:
# In your consuming project's package.json
"scripts": {
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File node_modules/@payez/next-mvp/scripts/dev-broker.ps1 -ClientId 2 -Port 3400"
}Which Script to Use?
| Scenario | Script | When to Use |
|----------|--------|-------------|
| 🎨 UI Development | dev:local | Building components, testing UI flows |
| 🔐 Auth Testing | dev:broker | Testing real OAuth flows, token management |
| 🚀 Production-like | dev:broker | Testing with actual IDP integration |
Quick Start (App Router with UI Components)
1. Configure Next.js
Add to your next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ['@payez/next-mvp'],
};
export default nextConfig;2. Set Environment Variables
Create .env.local (or .env.development) in your project root:
# PayEz IDP Configuration (required)
CLIENT_ID=your_client_slug_from_payez # e.g., "payez_idp_admin_web"
IDP_URL=https://idp.payez.net # or http://localhost:32785 for local dev
# NextAuth trusts request headers for OAuth callback URLs
AUTH_TRUST_HOST=true
# Secrets are resolved ENV FIRST, IdP client config second (PAY-1460, 4.4.6+).
# The IdP is retiring the secrets it serves in client config, so production
# deployments should set these from a k8s secret:
# BETTER_AUTH_SECRET=... # session signing secret (canonical name)
# NEXTAUTH_SECRET=... # legacy alias, used only if BETTER_AUTH_SECRET is unset
# GOOGLE_CLIENT_SECRET=... # per OAuth provider: <PROVIDER>_CLIENT_SECRET,
# APPLE_CLIENT_SECRET=... # provider name upper-cased, non-alphanumerics as "_"
# Startup is FATAL only when BOTH the env and the IdP config lack a signing secret.
# Values are never logged; the log names the source ("authSecret source: env:BETTER_AUTH_SECRET").
# Env-sourced secrets are never written to the Redis config cache.
# Optional
REDIS_URL=redis://localhost:6379 # For session storage
NEXT_PUBLIC_IDP_BASE_URL=https://idp.payez.net # For client-side redirectsNote:
NEXTAUTH_SECRETis intentionally omitted. The MVP automatically fetches it from the IDP at startup using the broker pattern. See Environment Variables Reference for details.
3. Create NextAuth API Route
Create app/api/auth/[...nextauth]/route.ts:
/**
* NextAuth API Route Handler
* Uses pre-configured handler from @payez/next-mvp
*/
export { GET, POST } from '@payez/next-mvp/routes/auth/nextauth';4. Wrap Your App with Providers
Create app/providers.tsx:
'use client';
import { SessionProvider } from 'next-auth/react';
import { ThemeProvider } from '@payez/next-mvp/theme';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</SessionProvider>
);
}Update app/layout.tsx:
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}5. Add Authentication Pages
Login Page - app/account-auth/login/page.tsx:
'use client';
import LoginPage from '@payez/next-mvp/dist/pages/login';
export default function LoginPageWrapper() {
return <LoginPage />;
}Password Recovery - app/account-auth/recovery/page.tsx:
'use client';
import RecoveryPage from '@payez/next-mvp/dist/pages/recovery';
export default function RecoveryPageWrapper() {
return <RecoveryPage />;
}Verify Code (2FA) - app/account-auth/verify-code/page.tsx:
'use client';
import VerifyCodePage from '@payez/next-mvp/dist/pages/verify-code';
export default function VerifyCodePageWrapper() {
return <VerifyCodePage />;
}What your app must serve (PAY-2121)
The IdP emails links that land on paths your app must host. Two of them are conventions the kit mounts for you; wire them or the link 404s at your site:
Invitation / onboarding - app/account-auth/onboarding/page.tsx:
// The IdP's welcome email links to
// {your site}/account-auth/onboarding?client_id=&onboard_token=
export { default } from '@payez/next-mvp/pages/onboarding';and the API it posts to (token-driven, no session) - one mount file each:
// app/api/onboarding/verify-code/route.ts
export { POST } from '@payez/next-mvp/routes/onboarding/verify-code';
// app/api/onboarding/send-email-code/route.ts
export { POST } from '@payez/next-mvp/routes/onboarding/send-email-code';
// app/api/onboarding/verify-welcome-email/route.ts
export { POST } from '@payez/next-mvp/routes/onboarding/verify-welcome-email';
// app/api/onboarding/set-password/route.ts
export { POST } from '@payez/next-mvp/routes/onboarding/set-password';Post-verify reset fragment on / - mount the bridge on your home page:
// app/page.tsx
import { PasswordResetBridge } from '@payez/next-mvp/components/auth/PasswordResetBridge';
export default function Home() {
return (<><PasswordResetBridge />{/* your home page */}</>);
}When the IdP's verification redirect lands on /#password_reset_required=true&reset_token=…&email=…
(a fragment, for a passwordless account), the bridge performs one replace to
/account-auth/recovery?token=…&email=… and clears the hash. With any other
fragment it renders nothing. npx @payez/create-next-app writes all of the above.
Registration has two doors, and the kit ships neither as a page:
- Owner-initiated invite (shipped here): an existing owner invites a user; the invite
email lands on
/account-auth/onboarding(above), which the kit now serves. - Self-serve sign-up: the kit exposes the IdP's ProgressiveAuth flow as API endpoints
(
/api/ProgressiveAuth/step1..step5, then/api/ProgressiveAuth/verify-email;API_ENDPOINTS.progressiveAuthinconfig/env.ts). The kit ships no registration page, so a developer who wants self-serve sign-up authors a page that drives that flow, respecting the tenant'sallow_public_registration/ waitlist state. The login page no longer links a register page, because the kit does not ship one.
6. Protect Routes with Authentication
Server-side protection:
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await getServerSession();
if (!session) {
redirect('/account-auth/login');
}
return (
<div>
<h1>Welcome, {session.user?.email}</h1>
</div>
);
}Client-side hook:
'use client';
import { useSession } from 'next-auth/react';
export default function ProfilePage() {
const { data: session, status } = useSession();
if (status === 'loading') return <div>Loading...</div>;
if (status === 'unauthenticated') return <div>Access Denied</div>;
return <div>Logged in as {session?.user?.email}</div>;
}Advanced: V2 Middleware-Based Setup
For applications requiring middleware-based route protection:
1. Configure Public Routes
Create src/lib/auth.ts:
import { configurePublicRoutes, createAuthOptions } from '@payez/next-mvp';
import CredentialsProvider from 'next-auth/providers/credentials';
// Define routes that do NOT require authentication
const publicRoutes = [
'/',
'/login',
'/api/health',
'/api/public/*', // Wildcard example
];
configurePublicRoutes(publicRoutes);
export const authOptions = createAuthOptions({
credentialsProvider: CredentialsProvider,
// Additional NextAuthOptions can be passed here
});
export { authOptions as GET, authOptions as POST } from './auth';2. Setup NextAuth API Route
For Pages Router (pages/api/auth/[...nextauth].ts):
import NextAuth from 'next-auth';
import { authOptions } from '../../../src/lib/auth';
export default NextAuth(authOptions);For App Router (app/api/auth/[...nextauth]/route.ts):
export { GET, POST } from '../../../src/lib/auth';3. Implement Middleware
Create middleware.ts at project root:
import { createMvpMiddleware } from '@payez/next-mvp';
export default createMvpMiddleware();
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};4. Use fetchWithAuth for Protected API Calls
import { fetchWithAuth } from '@payez/next-mvp';
async function fetchData() {
try {
const response = await fetchWithAuth('/api/protected-data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed to fetch protected data:', error);
}
}5. Session Viability API Handler
For Pages Router (pages/api/session/viability.ts):
import viabilityHandler from '@payez/next-mvp/api-handlers/session/viability';
export default viabilityHandler;For App Router (app/api/session/viability/route.ts):
export { default as GET } from '@payez/next-mvp/api-handlers/session/viability';Custom Theming
Customize branding and colors:
// lib/mvp-theme-config.ts
import { ThemeConfig } from '@payez/next-mvp/theme';
const customTheme: ThemeConfig = {
branding: {
companyName: "Your Company",
logoUrl: "/logo.svg",
logoAlt: "Your Company Logo"
},
colors: {
primary: "#3b82f6", // Blue
primaryHover: "#2563eb",
secondary: "#10b981", // Green
danger: "#ef4444", // Red
text: "#1f2937",
textLight: "#6b7280",
background: "#ffffff",
backgroundAlt: "#f9fafb",
border: "#e5e7eb"
},
layout: {
maxWidth: "28rem", // max-w-md
padding: "1.5rem", // p-6
borderRadius: "0.5rem" // rounded-lg
}
};
export default customTheme;Then use in your Providers:
import customTheme from '@/lib/mvp-theme-config';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<ThemeProvider theme={customTheme}>
{children}
</ThemeProvider>
</SessionProvider>
);
}API Routes
Access pre-built API handlers:
// Session validation
import { GET as validateSession } from '@payez/next-mvp/routes/auth/session';
// Token refresh
import { POST as refreshToken } from '@payez/next-mvp/routes/auth/refresh';
// Logout
import { POST as logout } from '@payez/next-mvp/routes/auth/logout';Environment Variables Reference
| Variable | Required | Description |
|----------|----------|-------------|
| CLIENT_ID | ✅ Yes | Your PayEz IDP client ID (string slug, e.g., payez_idp_admin_web) |
| IDP_URL | ✅ Yes | PayEz IDP base URL (e.g., https://idp.payez.net or http://localhost:32785) |
| AUTH_TRUST_HOST | ✅ Yes | Must be true - NextAuth derives OAuth URLs from request headers |
| NEXTAUTH_SECRET | 🔄 Broker | Do NOT set - fetched automatically from IDP at startup (see below) |
| CLIENT_SECRET | ⚠️ Legacy | Not needed with broker mode - IDP handles signing |
| NEXT_PUBLIC_IDP_BASE_URL | ⚠️ Optional | Public-facing IDP URL (for client-side redirects) |
| REDIS_URL | ⚠️ Optional | Redis connection string for session storage |
NEXTAUTH_SECRET Broker Flow
The MVP uses a "broker" pattern for NEXTAUTH_SECRET - the IDP securely provides it at startup rather than storing it in env files.
┌─────────────────────────────────────────────────────────────┐
│ App Startup (instrumentation.ts → ensureInitialized) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Is NEXTAUTH_SECRET set and non-empty? │
└─────────────────────────────────────────────────────────────┘
│ │
YES NO (recommended)
│ │
▼ ▼
┌─────────────┐ ┌─────────────────────────────────┐
│ Use as-is │ │ Fetch from IDP (Broker Mode) │
│ (override) │ │ 1. Sign client assertion │
│ │ │ 2. POST to /next-auth/secret │
│ │ │ 3. Set process.env at runtime │
└─────────────┘ └─────────────────────────────────┘Key points:
- Do NOT set
NEXTAUTH_SECRETin.envfiles - leave it undefined - The IDP provides the secret securely at startup via client assertion
- Secret is cached for 5 minutes, then re-fetched if needed
- If you DO set it manually, that value takes precedence (useful for overrides)
- The broker needs
CLIENT_IDandIDP_URLto fetch the secret
Troubleshooting
SessionProvider Error
If you see useSession must be wrapped in a <SessionProvider />:
- Ensure
transpilePackages: ['@payez/next-mvp']is innext.config.ts - Make sure all page components using PayEz components have
'use client'directive - Verify your app is wrapped with
<Providers>inlayout.tsx
Module Resolution Errors
If you get Module not found errors:
- Clear Next.js cache:
rm -rf .next - Reinstall dependencies:
npm install - Restart dev server
Authentication Not Working
- Verify
CLIENT_IDandIDP_URLare set correctly - Check
CLIENT_IDmatches your PayEz IDP configuration (use the slug, not numeric ID) - Verify IDP is running and reachable at
IDP_URL - Check server logs for broker startup messages - look for "NEXTAUTH_SECRET Successfully Fetched from IDP"
- If secret fetch fails, check IDP logs for client assertion errors
- Check browser console for error messages
NEXTAUTH_SECRET Issues
"NEXTAUTH_SECRET not available" error:
- This means the broker couldn't fetch the secret from IDP
- Check that
IDP_URLis correct and IDP is running - Verify
CLIENT_IDis registered in the IDP - Check network connectivity between your app and IDP
Cookie cross-contamination (localhost development):
- If running multiple apps on localhost (different ports), NextAuth cookies can conflict
- Clear
next-auth.*cookies in browser, or use incognito - Each app uses a unique cookie name based on CLIENT_ID to avoid conflicts
Docker/CI Deployment (IMPORTANT)
When consuming @payez/next-mvp via a local .tgz file in Docker builds, you must use a local path reference.
The Problem
If your package.json references the MVP like this:
"@payez/next-mvp": "file:../PayEz-Next-MVP/packages/next-mvp/payez-next-mvp-2.6.50.tgz"Docker builds will fail with:
npm error ENOENT: no such file or directory, open '/PayEz-Next-MVP/packages/next-mvp/payez-next-mvp-2.6.50.tgz'The relative path ../ resolves incorrectly inside the Docker container's /app working directory.
The Fix
Copy the
.tgzfile into your project root:cp ../PayEz-Next-MVP/packages/next-mvp/payez-next-mvp-*.tgz ./Update
package.jsonto use a local reference:"@payez/next-mvp": "file:./payez-next-mvp-2.6.50.tgz"Run
npm installto updatepackage-lock.jsonEnsure your Dockerfile copies the tgz:
COPY payez-next-mvp-*.tgz ./
Quick Fix Script
Run this in your consuming project to fix the path:
# Copy latest tgz
cp ../PayEz-Next-MVP/packages/next-mvp/payez-next-mvp-*.tgz ./
# Update package.json path (adjust version as needed)
sed -i 's|file:../PayEz-Next-MVP/packages/next-mvp/|file:./|g' package.json
# Regenerate lockfile
npm install --legacy-peer-depsAfter MVP Version Updates
When you update the MVP package version, repeat steps 1-3 to copy the new .tgz and update references.
Pre-Publishing Checklist (For Package Maintainers)
Before publishing this package to npm:
✅ Code Quality
- [x] All
@/path aliases converted to relative paths - [x] TypeScript compiles without errors:
npm run build - [x] No hardcoded configuration values (use env variables)
- [ ] Unit tests pass (if implemented)
📦 Package Configuration
CRITICAL FIXES NEEDED:
Fix
mainfield in package.json (Line 5):"main": "dist/index.js", // NOT "src/index.ts"Remove unused
tsc-aliasfrom devDependencies (Line 542):// DELETE this line: "tsc-alias": "^1.8.16"Add package metadata:
{ "description": "PayEz IDP authentication package for Next.js with ready-to-use login, recovery, and profile pages", "keywords": ["nextjs", "authentication", "next-auth", "payez", "idp", "oauth"], "author": "PayEz Team", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/your-org/PayEz-Next-MVP" }, "homepage": "https://github.com/your-org/PayEz-Next-MVP#readme", "bugs": { "url": "https://github.com/your-org/PayEz-Next-MVP/issues" } }
📝 Documentation
- [x] README.md is complete and accurate
- [ ] CHANGELOG.md exists with version history
- [ ] LICENSE file exists
🧪 Testing
- [ ] Test fresh installation in new Next.js 14 project
- [ ] Test fresh installation in new Next.js 15 project
- [ ] Verify all exported modules work
- [ ] Test with React 18 and React 19
- [ ] Test package build:
npm pack --dry-run
🚀 Publishing Steps
# 1. Fix package.json issues (see above)
# 2. Login to npm
npm login
# 3. Build package
npm run build
# 4. Preview what will be published
npm pack --dry-run
# 5. Test the tarball locally
npm install ./payez-next-mvp-2.3.1.tgz
# 6. Bump version (patch/minor/major)
npm version patch # 2.3.1 -> 2.3.2
npm version minor # 2.3.1 -> 2.4.0
npm version major # 2.3.1 -> 3.0.0
# 7. Publish to npm
npm publish
# 8. Create git tag and push
git push origin main
git push origin --tags📋 .npmignore
Create .npmignore to exclude unnecessary files:
src/
*.test.ts
*.test.tsx
tsconfig.json
.gitignore
.git
node_modules/
*.tgz
.env*🔒 Publish Configuration
Add to package.json:
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}📚 Complete Documentation
Core Guides
THEMING.md - Complete guide to customizing auth components
- Theme structure and configuration
- Creating light/dark modes
- Component-specific customization
- Common theming issues and solutions
CSS_VARIABLES_INTEGRATION.md - CSS variable injection for MVP components
- Why CSS variables are needed
- How to inject variables in your app
- Color conversion utilities
- Debugging CSS variable issues
THEME_EXAMPLES.md - Ready-to-use theme examples
- 7 pre-built professional themes
- Multi-brand theme support
- Dynamic light/dark mode
- Copy-paste theme configurations
Topics Covered
Theming
- ✅ Brand colors and logos
- ✅ Light and dark modes
- ✅ Font families and typography
- ✅ Layout and spacing customization
- ✅ Component-specific styling
- ✅ Multi-tenant/white-label setups
- ✅ Accessibility and contrast
CSS Variables
- ✅ Understanding CSS custom properties
- ✅ Manual variable injection
- ✅ Color conversion (Tailwind → hex)
- ✅ Theme provider pattern
- ✅ Dynamic theme switching
- ✅ Debugging in DevTools
Examples
- ✅ Corporate themes
- ✅ SaaS themes
- ✅ Creative/design themes
- ✅ Accessible high-contrast themes
- ✅ Multi-brand configurations
- ✅ Gradient backgrounds
- ✅ Custom branding
Quick Links
Getting Started with Themes:
- Start with THEMING.md - Quick Start section
- Copy a theme from THEME_EXAMPLES.md
- Follow integration pattern in CSS_VARIABLES_INTEGRATION.md
For Specific Tasks:
- Want to change colors? → THEMING.md - Creating a Theme
- Pages rendering white/no color? → CSS_VARIABLES_INTEGRATION.md - Troubleshooting
- Need a complete example? → THEME_EXAMPLES.md
- Debugging styling issues? → THEMING.md - Common Issues
License
MIT
Support
For issues and questions:
- GitHub Issues: https://github.com/your-org/PayEz-Next-MVP/issues
- Documentation: https://docs.payez.net
Contributing
Contributions welcome! Please read our contributing guidelines first.
Made with ❤️ by the PayEz Team
