Building a Complete Niche Subscription Box Platform with Next.js 14, Prisma, and Tailwind

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:

  1. Register or login as user@nichebox.com / user123
  2. Browse products
  3. Subscribe to one
  4. 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!