In this comprehensive guide, we’ll build a complete digital product store using Next.js 16, Prisma, and JWT authentication — step by step. Users will be able to register, log in, browse products, simulate checkout (no Stripe required), and download their digital assets via a real “My Downloads” page.
⚠️ Note: The full project code is too large to include here. You can download it directly from Google Drive:
👉 Download Full Project Code (Google Drive)
Use it to explore the complete implementation, database setup, and API routes.
Tech Stack
- Next.js 16 (App Router) – Full-stack framework (frontend + backend)
- Prisma ORM – Database toolkit
- SQLite / PostgreSQL – Persistent storage
- React Query (@tanstack/react-query) – Client-side data fetching
- JWT (jsonwebtoken) – Token-based authentication
- Tailwind CSS – Styling
Step 1: Setup the Project
Start a new Next.js project and install dependencies:
npx create-next-app@latest my-app
cd my-app
npm install prisma @prisma/client jsonwebtoken bcryptjs @tanstack/react-query axios
npm install --save-dev ts-node typescript
npx prisma init
Step 2: Configure Prisma Schema
Edit your prisma/schema.prisma to define the data models:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
purchases Purchase[]
createdAt DateTime @default(now())
}
model Product {
id Int @id @default(autoincrement())
title String
slug String @unique
description String?
priceCents Int
currency String
filePath String
isPublished Boolean @default(true)
createdAt DateTime @default(now())
purchases Purchase[]
}
model Purchase {
id Int @id @default(autoincrement())
userId Int
productId Int
amountCents Int
currency String
status String
licenseKey String
createdAt DateTime @default(now())
product Product @relation(fields: [productId], references: [id])
user User @relation(fields: [userId], references: [id])
}
Then migrate:
npx prisma migrate dev --name init
Step 3: Authentication with JWT
We’ll create two API routes — register and login — that use bcrypt for password hashing and JWT for authentication.
src/pages/api/auth/register.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import bcrypt from 'bcryptjs'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })
const { name, email, password } = req.body
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) return res.status(400).json({ error: 'Email already registered' })
const hashed = await bcrypt.hash(password, 10)
const user = await prisma.user.create({ data: { name, email, password: hashed } })
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET || 'secret', { expiresIn: '7d' })
res.status(200).json({ token })
}
src/pages/api/auth/login.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import bcrypt from 'bcryptjs'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })
const { email, password } = req.body
const user = await prisma.user.findUnique({ where: { email } })
if (!user) return res.status(400).json({ error: 'Invalid credentials' })
const valid = await bcrypt.compare(password, user.password)
if (!valid) return res.status(400).json({ error: 'Invalid credentials' })
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET || 'secret', { expiresIn: '7d' })
res.status(200).json({ token })
}
Step 4: Seeding Demo Products
Create a prisma/seed.ts script:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
await prisma.product.createMany({
data: [
{
title: 'UI Kit Starter',
slug: 'ui-kit-starter',
description: 'A clean and modern UI kit for developers',
priceCents: 1500,
currency: 'usd',
filePath: 'ui-kit-starter.zip',
},
{
title: 'Mastering CSS Ebook',
slug: 'mastering-css',
description: 'A complete guide to modern CSS',
priceCents: 900,
currency: 'usd',
filePath: 'mastering-css.pdf',
},
],
})
}
main().finally(() => prisma.$disconnect())
Run:
npm run prisma:seed
Step 5: Demo Checkout (No Stripe)
We’ll simulate payments by directly creating a Purchase entry in the database.
src/pages/api/purchase/create-session.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })
const auth = req.headers.authorization
if (!auth) return res.status(401).json({ error: 'No token' })
const token = auth.split(' ')[1]
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret') as any
const userId = decoded.id
const { productId } = req.body
const product = await prisma.product.findUnique({ where: { id: productId } })
if (!product) return res.status(404).json({ error: 'Product not found' })
const purchase = await prisma.purchase.create({
data: {
userId,
productId,
amountCents: product.priceCents,
currency: product.currency,
status: 'complete',
licenseKey: `LIC-${Math.random().toString(36).substring(2, 10).toUpperCase()}`,
},
include: { product: true },
})
res.status(200).json({ redirect: '/downloads', purchase })
}
Step 6: My Downloads API
src/pages/api/purchase/list.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })
const auth = req.headers.authorization
if (!auth) return res.status(401).json({ error: 'No token' })
const token = auth.split(' ')[1]
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret') as any
const userId = decoded.id
const purchases = await prisma.purchase.findMany({
where: { userId },
include: { product: true },
orderBy: { createdAt: 'desc' },
})
res.status(200).json({ items: purchases })
}
Step 7: Frontend Pages
Home Page
Displays products with “Buy Now” buttons fetched via /api/products.
Checkout Page
Calls the demo checkout API and redirects to /downloads.
Downloads Page
Fetches /api/purchase/list and lists purchased products with download links from /public/files.
Step 8: Fix Hydration Issues
Always call hooks in the same order and use enabled in useQuery:
const { data } = useQuery({ queryFn, enabled: mounted && !!token })
Step 9: Test Everything
npm run dev- Register & login
- Buy a product → Simulate payment
- Go to My Downloads
- Download demo files 🎉
Final Features
- JWT-based authentication
- Prisma ORM integration
- Product management
- Demo checkout (no Stripe)
- Downloadable content
- Hydration-safe React Query
Conclusion
You’ve just built a complete digital product store using Next.js, Prisma, and JWT — with authentication, fake payments, and downloadable assets.
Enjoy coding your own Gumroad-style digital store!
