A Next.js + Prisma sample app for managing haircuts, purchases, points and coupons. This repository contains both customer-facing pages and an admin panel with content management (haircuts) and transactional approval flows.
- What is this
- Key features
- Tech stack
- Prerequisites
- Environment variables
- Setup (development)
- Database (Prisma) commands
- Running the app
- Important API routes
- Typical manual test flow
- Troubleshooting
- Contributing
- License
BarberHub is a small web application built with the Next.js App Router that demonstrates a booking/points/coupons workflow for a barber shop. Customers can register, book haircuts (which create pending point transactions), and redeem points for coupons. Admins can approve transactions, verify coupons, and manage haircuts (including uploading images to Cloudinary).
- Customer registration/login (using username and password)
- Admin registration/login (protected by an admin secret)
- Bookings that create pending points transactions
- Admin approval workflow that atomically updates user
pointsBalance - Coupon purchase flow that atomically deducts points and creates coupon records
- Cloudinary signed uploads for admin-managed haircut images
- Prisma-backed persistence (PostgreSQL recommended)
- Next.js (App Router) — React + TypeScript
- Prisma ORM + PostgreSQL (or other supported DB)
- Cloudinary for image hosting (signed uploads)
- JSON Web Tokens (JWT) + bcrypt for authentication
- TailwindCSS + Radix UI for UI primitives
- Node.js (v18+ recommended)
- pnpm (preferred; repo scripts expect
pnpm) — you can still use npm/yarn but commands below use pnpm - A PostgreSQL database (or other database supported by Prisma); ensure
DATABASE_URLis set - Cloudinary account (for image uploads) —
CLOUDINARY_URLenv var or separate keys
Create a .env file in the project root (do NOT commit secrets). The app expects at least the following keys:
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public"
JWT_SECRET="a-very-secret-key"
ADMIN_SECRET_KEY="your-admin-secret-key"
CLOUDINARY_URL="cloudinary://API_KEY:API_SECRET@CLOUD_NAME"
Notes:
- The project uses
CLOUDINARY_URL(Cloudinary's single-string connection) for the server-side signing endpoint. If you prefer separate env vars, you can adapt the server sign endpoint accordingly.
Install dependencies:
pnpm installSet up the database schema and the Prisma client. The repo provides several handy scripts in package.json.
You can run a full DB setup (generate client + run migrations interactively):
pnpm db:setupOr run the commands individually:
pnpm prisma:generate # generate Prisma client
pnpm prisma:migrate # create/run migrations (interactive)
# or, if you prefer not to use migrations:
pnpm prisma:push # push Prisma schema to DB (no migration history)To open Prisma Studio (DB GUI):
pnpm prisma:studioIf you already have production data and are adding the pointsBalance field to User, consider backfilling it from existing approved Points rows. If you'd like, I can provide a small script to compute and write pointsBalance from approved transactions.
Start the dev server:
pnpm devBuild for production:
pnpm build
pnpm startpnpm dev— run Next.js in development modepnpm build— build the app for productionpnpm start— start the built apppnpm prisma:generate— generate Prisma clientpnpm prisma:migrate— run interactive migrationspnpm prisma:push— push schema to DBpnpm prisma:studio— open Prisma Studio
These routes are implemented under app/api/ (App Router API routes). Key endpoints include:
POST /api/register— customer registration (sendsusername+ password)POST /api/login— customer loginPOST /api/admin/register— admin registration (requiresADMIN_SECRET_KEY)POST /api/admin/login— admin loginGET /api/haircuts— list haircuts (services)POST /api/haircuts— (admin) create a haircut entry (image URL + metadata)POST /api/purchases— customer books a haircut; creates a pending Points transactionGET /api/admin/transactions— admin: list pending/previous transactionsPOST /api/admin/validate-points— admin: approve/deny a pending transaction; approval atomically updates the user'spointsBalancePOST /api/coupons— customer: buy a coupon (transactionally decrementspointsBalanceand creates a coupon)POST /api/admin/verify-coupon— admin: verify (redeem/mark used) a couponPOST /api/admin/cloudinary/sign— admin-only: generate signature and timestamp for client-side signed uploads to Cloudinary
Refer to the code under app/api/* for the full implementations and any required request shapes.
- Start the app and ensure the DB is migrated and Prisma client generated.
- As a customer:
- Register a user (username + password).
- Log in and book a haircut (this will create a pending transaction).
- As an admin:
- Register/Log in as an admin (requires
ADMIN_SECRET_KEYduring registration). - Open the admin panel and approve the pending transaction — this should atomically increase the customer's
pointsBalance.
- Register/Log in as an admin (requires
- As the customer:
- Check your points balance and redeem points for a coupon.
- As admin:
- Verify the coupon code and mark it used.
If you use the admin haircuts UI, create a Cloudinary-signed upload (the UI calls /api/admin/cloudinary/sign to get a signature and then uploads to Cloudinary directly). After upload, the admin UI sends the resulting secure_url to POST /api/haircuts to create the haircut record.
- Dev server fails with TypeScript errors related to
pointsBalance:- This usually means you added
pointsBalancetoprisma/schema.prismabut haven't runprisma generateor applied the migration. Run:
- This usually means you added
pnpm prisma:generate
pnpm prisma:migrateprismaclient type mismatch after schema change: re-runpnpm prisma:generate.- Cloudinary uploads failing: verify
CLOUDINARY_URLis set and the server can parse it. For troubleshooting, confirm the API key/secret/cloud name are correct in the URL (cloudinary://API_KEY:API_SECRET@CLOUD_NAME). - Race conditions or negative
pointsBalance: the server performs transactional updates for approvals and coupon purchases. If you see unexpected negative values, ensure your DB operations are not being bypassed and that migrations were applied successfully.
Contributions are welcome. A minimal suggested workflow:
- Fork the repo.
- Create a feature branch:
git checkout -b feat/your-feature. - Make changes and add tests where appropriate.
- Run the dev server and verify behavior.
- Open a pull request with a clear description of the change.
If you're adding or changing the Prisma schema, please include migration files or document how to apply changes locally.