# Creating a Custom Payment Gateway Integration with Next.js and Permit.io Building a secure, role-aware payment experience with Firebase + Permit.io + Next.js. ## Why This Project? You've probably seen dozens of payment tutorials that stop at "add Stripe, hit submit." Cool, but what if you want to: * Lock down the payment route so only authorized users can access it? * Dynamically show different UI based on user roles (e.g., customer, admin)? * Actually make it secure with real authentication and authorization? That's what this guide is about. * We're going to: * Use [Next.js App Router](https://nextjs.org/docs/app) for UI + routing * Plug in [Firebase](https://firebase.google.com/) for auth (sign up, log in) * Layer in Permit.io to enforce access based on user roles * Deploy it on [Vercel](https://vercel.com/), ready for the real world. ***You can play around with the live application [here](https://custom-payment-gateway-tau.vercel.app/) and access the code in this [github repo](https://github.com/Bannieugbede/custom-payment-gateway/). ![image](https://hackmd.io/_uploads/SyyFTt-kxg.png)*** ## Getting Started Step 1: Create the [Next.js](https://www.permit.io/blog/how-to-protect-a-url-inside-a-nestjs-app-using-rbac-authorization) App ```bash npx create-next-app@latest custom-payment-gateway cd custom-payment-gateway ``` Step 2: Install Dependencies ```bash npm install firebase firebase-admin axios permitio tailwindcss postcss autoprefixer npx tailwindcss init -p ``` Step 3: Setup Tailwind CSS ```bash tailwind.config.js content: [ "./app/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}" ], Create app/globals.css with base styles: body { font-family: sans-serif; background-color: #f9f9f9; color: #111; } ``` ## Firebase Auth Setup You'll use [Firebase authentication](https://www.permit.io/blog/authentication-and-authorization-with-firebase) for signing users up and generating ID tokens. In your Firebase project: * Enable Email/Password Auth * Generate a service account key (used by server-side Firebase Admin SDK) Then set these as environment variables: ```javascript FIREBASE_PROJECT_ID=your_project_id FIREBASE_CLIENT_EMAIL=your_email FIREBASE_PRIVATE_KEY="your_key" ``` Use these to initialize Firebase Admin SDK in `lib/firebase-admin.ts:` ```firebase import { cert, getApps, initializeApp } from 'firebase-admin/app'; import { getAuth } from 'firebase-admin/auth'; if (!getApps().length) { initializeApp({ credential: cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n') }) }); } export const adminAuth = getAuth(); ``` This code initializes the [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup) using credentials from environment variables, ensuring it only runs once even if the file is imported multiple times. It then exports the `adminAuth` instance, which provides access to Firebase Authentication functions for server-side use. ## Setting Up Permit.io - Access Control, Sorted Think of [Permit.io](https://www.permit.io/) as your app's bouncer. It decides who can enter VIP (your protected pages). Step 1: Create a Free Permit.io Project Go to permit.io → Sign up → Create project → Add environment → Add resource payment ![image](https://hackmd.io/_uploads/Hy5nTFZyel.png) Permit.io provides a clear separation between development and production environments, helping teams manage access control safely. In the Development environment, access enforcement is active, with 5 users, 3 defined roles, and 1 connected resource. The Production environment, however, is still in the setup phase, awaiting user connections and role configurations. This structure ensures that changes can be tested thoroughly before being pushed live, reducing the risk of security issues or disruptions. Step 2: Define Actions + Roles ``` resource: payment actions: - pay - cancel - process roles: - customer - admin ``` ![image](https://hackmd.io/_uploads/Byo06Kb1lx.png) Above here shows the key roles and the access they have. The Permit.io dashboard provides a detailed view of the roles and permissions assigned across environments. In the Development environment, three roles are actively managing access control among five users, with enforced access already enabled. Each role defines specific access policies and governs how users interact with the system resources. Meanwhile, the Production environment is configured but not yet enforcing access, ensuring that permissions can be carefully tested and reviewed before deployment to live users. Step 3: Connect SDK to Your App `Create lib/permit.ts:` ```next.js import { Permit } from '@permit.io/sdk'; const permit = new Permit({ token: process.env.PERMIT_API_KEY, pdp: process.env.PERMIT_IO_PDP_URL, }); export default permit; ``` Step 4: Assign Roles via API The below happens automatically when a user signs up ```javascript await permit.api.users.sync({ key: email, email, first_name: name, }); await permit.api.roles.assign(role, email); ``` Done. You've got policy-based access without writing middleware logic. ## Signup + Login Flows (Firebase + Permit.io) Let's wire up the user flows. Both use Firebase Auth on the frontend, with Permit.io logic handled via API routes. ### Signup Page The inclusion of a password visibility toggle enhances usability, allowing users to verify their input without compromising security. Additionally, the role selector is a thoughtful touch that enables customized user experiences based on their function within the platform. ```javascript // src/app/signup/page.tsx const handleSignup = async () => { const userCredential = await createUserWithEmailAndPassword(auth, email, password); const user = userCredential.user; await updateProfile(user, { displayName: name }); const idToken = await user.getIdToken(); await axios.post("/api/signup", { idToken, role, email: user.email, name, }); router.push("/payment"); }; ``` This code defines an asynchronous handleSignup function that manages user registration using Firebase Authentication. It creates a new user with an email and password, updates their profile with a display name, and retrieves an ID token for authentication. The function then sends the user's details, including role and token, to a backend API for further processing. Finally, it redirects the user to the payment page, completing the signup workflow. ![image](https://hackmd.io/_uploads/rywrCF-klx.png) The above shows a user signup page, this page shown above exemplifies a clean, intuitive, and user-centric approach to onboarding users onto a digital payment platform. With a minimalist layout and clear input fields, new users can easily create an account by entering their full name, email, password, and selecting their role (e.g., Customer, Admin, etc.) from a dropdown menu. `/api/signup` Route ```javascript const { idToken, email, role, name } = await req.json(); await auth.verifyIdToken(idToken); await permit.api.users.sync({ key: email, email, first_name: name }); await permit.api.roles.assign(role, email); ``` Login Page ```javascript // src/app/login/page.tsx const handleLogin = async () => { const userCredential = await signInWithEmailAndPassword(auth, email, password); const idToken = await userCredential.user.getIdToken(); await axios.post("/api/set-token", { idToken }); router.push("/payment"); }; ``` `/api/set-token` Route ```javascript cookies().set("idToken", idToken, { httpOnly: true, secure: true }); ``` ## Protected Payment Page (Only If You're Allowed) Now for the fun part - creating the payment form and locking it behind role-based access control. `/payment/page.tsx` ```const idToken = cookies().get("idToken")?.value; const decoded = await adminAuth.verifyIdToken(idToken); const email = decoded.email; const hasAccess = await permit.check(email, "pay", "payment"); if (!hasAccess) { return <p>You do not have permission to view this page.</p>; } return <PaymentForm />; ``` `/components/PaymentForm.tsx` ```javascript ```javascript const handleSubmit = async (e: FormEvent) => { e.preventDefault(); const res = await axios.post("/api/payment", { name, email, amount }); setResponse(res.data); }; ``` The `/payment/page.tsx` code verifies the user's identity by decoding their Firebase ID token using adminAuth. It extracts the user's email and checks their permission to access the payment page through a role-based access control system (permit.check). If unauthorized, it displays a permission error message; otherwise, it renders the PaymentForm component. Inside `PaymentForm.tsx`, the handleSubmit function handles form submissions by preventing the default behavior and sending a POST request to the `/api/payment` endpoint with the user's name, email, and payment amount, then stores the server response for further use. ![image](https://hackmd.io/_uploads/ry3vy5Z1gg.png) Payment form for user to input payment details. This interface offers a clean and user-friendly form for completing transactions. Once logged in, users are prompted to provide essential details such as cardholder name, role, and the desired payment amount. The system dynamically calculates the payment fee and total amount due, giving users a transparent breakdown before proceeding. This layout ensures a smooth and secure payment process, reinforcing trust and efficiency in the user experience. ![image](https://hackmd.io/_uploads/rybhJ5bJxg.png) Above is the view of the login page. This page of the Payment Gateway platform is designed with simplicity and functionality in mind. Users are required to enter their registered email and password to gain access to their accounts. The interface includes a password visibility toggle for ease of use and a direct link to the signup page for new users. With its clean layout and intuitive design, the page ensures a seamless and secure authentication process for returning users. **Enforce HTTPS and TLS in Production** In a production environment, it’s crucial to enforce [HTTPS and use TLS](https://www.goanywhere.com/blog/what-is-ssl-tls-and-https) (Transport Layer Security) to protect data in transit. This ensures that all communication between the client and server is encrypted, preventing sensitive information like authentication tokens and payment details from being exposed to potential attackers. Always configure your server to redirect all HTTP traffic to HTTPS and keep your TLS certificates updated for maximum security. ```javascript // next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { async headers() { return [ { source: "/:path*", headers: [ { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload", }, ], }, ]; }, }; module.exports = nextConfig; ``` **HTTPS and TLS Encryption:** * Enforced HTTPS in production via `next.config.js` and Vercel. * Local development supports HTTPS with [mkcert](https://mkcert.org/). Validating User Input and Preventing Web Vulnerabilities: * Client-side validation in PaymentForm for all fields. * Server-side validation and sanitization in /api/payment to prevent XSS and injection attacks. * Secure headers in the API route to mitigate [clickjacking](https://www.imperva.com/learn/application-security/clickjacking/#), [XSS, and MIME-type sniffing](https://aszx87410.github.io/beyond-xss/en/ch5/mime-sniffing/). * Custom rate limiting to prevent abuse. `/api/payment/route.ts` ```javacript const { amount, email, name } = await req.json(); console.log(`Processing $${amount} for ${name}`); return new Response(JSON.stringify({ success: true, message: "Payment successful", name, email, amount }), { status: 200 }); ``` This code snippet reads `JSON` data from a request, extracting the `amount`, `email`, and `name` fields. It then logs a message indicating the payment is being processed. Finally, it sends back a JSON response confirming the payment was successful, along with the provided `name`, `email`, and `amount`, and sets the HTTP status code to 200 (OK). ![image](https://hackmd.io/_uploads/Sk2LU-YJgl.png) The below image shows the view of a successful payment, after authentication user, fee and card details. After completing the payment and authentication process, the user is presented with a confirmation screen indicating that the transaction was successful. This screen serves as a receipt and includes key details such as the cardholder's name, email address, total amount paid, and a unique transaction reference number. It reassures the user that their payment has been securely processed and provides clear, organized information for future reference. Additionally, the user is given options to either print the receipt for their records or return to the login page, ensuring a smooth and professional user experience within the payment gateway system. ## Auth + Access Flow Diagram In this below flow, when a user signs up or logs in, they receive a Firebase authentication token, which is then stored as a cookie. Upon visiting the /payment page, the server verifies the token's validity and checks with Permit.io whether the user is authorized to perform the "pay" action on the "payment" resource. If the user has the necessary permissions, the payment form is shown; otherwise, the user is blocked and receives an Unauthorized error. ``` User → Signup/Login → Get Firebase Token → Set Cookie → Visit /payment → Server Verifies Token → Permit.io: Can this user "pay" on "payment"? ├── Yes → Show Payment Form └── No → Block with Unauthorized ``` ## Deploying to Production (Vercel FTW) Step 1: Push Code to GitHub ```bash git init git remote add origin https://github.com/your/repo.git git push -u origin main ``` Step 2: Connect GitHub to Vercel * Go to vercel.com * Connect your GitHub repo * Add your environment variables in the Vercel dashboard: ```bash # Permit.io Configuration (server-side) PERMIT_API_KEY=<your-permit-api-key> PERMIT_IO_PDP_URL=<permit-io-PDP-URL> # Firebase Admin SDK (server-side) FIREBASE_PROJECT_ID=<your-firebase-project-id> FIREBASE_CLIENT_EMAIL=<your-firebase-client-email> FIREBASE_PRIVATE_KEY="your-firebase-private-key" # Custom Payment Gateway (server-side) PAYMENT_GATEWAY_SECRET=<your-custom-payment-secret> Create or compose a unique key Replace placeholders with your actual credentials. Note: PERMIT_API_KEY and PAYMENT_GATEWAY_SECRET are server-side to ensure security. ``` Step 3: Deploy Hit Deploy and watch the magic happen. Feel free to test and explore the live application click-here. ### Monitor & Debug Like a Pro Monitoring and debugging like a pro means going beyond basic logs—it's about gaining full visibility into your application's behavior. * `Use console.log()` in both frontend and backend to trace payment flow * Monitor Firebase logs from the Firebase console * Check Permit.io audit trail for denied actions * Set breakpoints in Vercel's deployment logs if needed Final Thoughts: You Did It. ![2025-04-0917-05-36-ezgif.com-resize](https://hackmd.io/_uploads/H19BDeFkel.giff) Above here is a snippet of the payment gateway we just built: Seamless payment integration is more than just connecting services — it's about creating a secure, role-aware, and smooth experience for both users and developers. In the next sections, we'll walk through how to achieve this with Next.js, Firebase, and Permit.io. **We just built a:** * Full-stack app with real authentication * Real-time [authorization](https://vercel.com/) using roles * Secure payment gateway (mocked) * Production-ready deployment which can be viewed [here](https://custom-payment-gateway-tau.vercel.app)