In this guide, we’ll walk through how to build a Niche Subscription Box Platform — similar to a service that delivers hot sauces, vinyl records, or coffee subscriptions — using Next.js 14, Prisma, and Tailwind CSS. We’ll build a backend API, connect it to a SQLite database, and create a full frontend for customers and admins.
This article is perfect for developers who want to learn how to combine modern full-stack tools into a production-ready subscription system.
Tech Stack
- Next.js 14 (App Router) — for both backend (API routes) and frontend pages
- Prisma ORM — for database schema, migrations, and queries
- SQLite — simple local database
- JWT (jsonwebtoken) — for authentication and authorization
- Axios — for frontend API requests
- Tailwind CSS — for responsive styling
Project Setup
npx create-next-app@latest my-app --typescript
cd my-app
npm install prisma @prisma/client bcryptjs jsonwebtoken axios tailwindcss
npx prisma init --datasource-provider sqlite
npx tailwindcss init -p
Your folder structure should look like this:
my-app/
├─ prisma/
│ ├─ schema.prisma
├─ src/
│ ├─ app/
│ │ ├─ api/
│ │ ├─ components/
│ │ ├─ lib/
│ │ └─ pages
│ └─ lib/
├─ .env
├─ package.json
└─ tailwind.config.ts
Database and Prisma Setup
Edit your .env file:
DATABASE_URL="file:./dev.db"
JWT_SECRET="your_super_secret_key_here"
STRIPE_SECRET_KEY="sk_test_your_key_here"
Now define your Prisma schema (prisma/schema.prisma):
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String
name String?
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
subs Subscription[]
payments Payment[]
}
model Product {
id String @id @default(cuid())
name String
description String?
priceCents Int
metadata Json?
createdAt DateTime @default(now())
subscriptions Subscription[]
}
model Subscription {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
product Product @relation(fields: [productId], references: [id])
productId String
interval String
active Boolean @default(true)
startedAt DateTime @default(now())
nextShipment DateTime?
shipments Shipment[]
}
model Shipment {
id String @id @default(cuid())
subscription Subscription @relation(fields: [subscriptionId], references: [id])
subscriptionId String
shippedAt DateTime?
status String @default("pending")
trackingNumber String?
}
model Payment {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
amountCents Int
currency String @default("usd")
method String
succeeded Boolean @default(false)
createdAt DateTime @default(now())
}
Run the following commands:
npx prisma migrate dev --name init
npx prisma generate
Authentication with JWT
Inside src/lib/auth.ts:
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { prisma } from './prisma';
const JWT_SECRET = process.env.JWT_SECRET ?? 'dev-secret';
export function signJwt(payload: object, options?: jwt.SignOptions) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d', ...(options ?? {}) });
}
export function verifyJwt(token: string) {
try {
return jwt.verify(token, JWT_SECRET) as any;
} catch (e) {
return null;
}
}
export async function getUserFromRequest(req: NextRequest) {
const auth = req.headers.get('authorization') || '';
if (!auth.startsWith('Bearer ')) return null;
const token = auth.replace('Bearer ', '');
const payload = verifyJwt(token);
if (!payload?.sub) return null;
const user = await prisma.user.findUnique({ where: { id: payload.sub } });
return user;
}
Building the Backend API
You’ll create endpoints for:
- /api/auth/register → Create user
- /api/auth/login → Login & return JWT
- /api/products → Public product list
- /api/subscriptions → Protected, user-specific subscriptions
Example: src/app/api/auth/register/route.ts
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';
import { signJwt } from '@/lib/auth';
export async function POST(req: NextRequest) {
const { email, password, name } = await req.json();
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) return NextResponse.json({ error: 'Email already registered' }, { status: 400 });
const hashed = await bcrypt.hash(password, 10);
const user = await prisma.user.create({ data: { email, password: hashed, name } });
const token = signJwt({ sub: user.id, email: user.email });
return NextResponse.json({ token, user });
}
Similar structure applies to login, product listing, and subscription creation.
Building the Frontend
Use Tailwind to design minimal pages for:
- Login & Register
- Product Listing
- Subscriptions Page
- Admin Dashboard
Example: src/app/products/page.tsx
'use client';
import { useEffect, useState } from 'react';
import api from '@/lib/api';
import ProductCard from '@/components/ProductCard';
export default function ProductsPage() {
const [products, setProducts] = useState<any[]>([]);
useEffect(() => {
api.get('/products').then((res) => setProducts(res.data));
}, []);
return (
<div>
<h1 className="text-3xl font-semibold mb-4">Products</h1>
<div className="grid md:grid-cols-3 gap-4">
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
);
}
Example: src/components/ProductCard.tsx
'use client';
import api from '@/lib/api';
export default function ProductCard({ product }: { product: any }) {
async function subscribe() {
const interval = prompt('Enter interval (e.g., monthly)');
if (!interval) return;
await api.post('/subscriptions', { productId: product.id, interval });
alert('Subscribed!');
}
return (
<div className="bg-white p-4 rounded shadow">
<h2 className="text-lg font-semibold">{product.name}</h2>
<p className="text-gray-600">{product.description}</p>
<p className="font-bold mt-2">${(product.priceCents / 100).toFixed(2)}</p>
<button onClick={subscribe} className="mt-2 bg-blue-600 text-white px-3 py-1 rounded">
Subscribe
</button>
</div>
);
}
Seeding Sample Data
You can quickly seed test data with a Prisma script (prisma/seed.ts):
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.deleteMany();
await prisma.product.deleteMany();
await prisma.user.create({
data: {
email: 'user@nichebox.com',
password: '$2a$10$jxBqB6aPZw6mHDFRL5Mi4OifH/4SKNhMUKFUsI8Ze62O9fLCnbXz6', // 'user123'
name: 'Regular User',
},
});
await prisma.product.createMany({
data: [
{ name: 'Hot Sauce Box', description: 'Spicy sauces every month', priceCents: 2999 },
{ name: 'Vinyl Record Box', description: 'Exclusive records monthly', priceCents: 3999 },
{ name: 'Coffee Box', description: 'Fresh roasts delivered weekly', priceCents: 2499 },
],
});
console.log('✅ Seed complete!');
}
main().finally(() => prisma.$disconnect());
Run it:
npx prisma db seed
Running the App
npm run dev
Then open http://localhost:3000
Try it out:
- Register or login as
user@nichebox.com / user123 - Browse products
- Subscribe to one
- View active subscriptions
Conclusion
You’ve just built a complete subscription box platform with:
- Secure JWT authentication
- Prisma ORM database
- RESTful API routes using Next.js App Router
- Responsive frontend with Tailwind
From here, you can enhance your app by:
- Adding Stripe Checkout for real payments
- Sending emails for shipments
- Adding NextAuth for social login
- Deploying to Vercel with a managed Postgres database
This project is a powerful full-stack template for any kind of subscription-based business.
Note
I can’t include the entire project code directly here due to size constraints. However, you can download the full project from Google Drive and explore it in detail:
👉 Download the complete project from Google Drive
Enjoy exploring and building your own subscription box project!
