A Next.js app for managing a restaurant wine bar's back of house: inventory, vendors, purchase orders, and vendor invoices (logged as expenses), all surfaced on a dashboard.
- Next.js 16 (App Router, React Server Components, Server Actions) + TypeScript
- Prisma 7 ORM with PostgreSQL (Supabase) via the
@prisma/adapter-pgdriver adapter - Tailwind CSS 4 + shadcn/ui (base-ui primitives, lucide icons)
- Clerk for authentication (email + Google) and Organizations for multi-tenancy
- Zod for server-side validation
pnpm install # installs deps; postinstall runs `prisma generate`
# set DATABASE_URL (+ Clerk keys) in .env — see Database and .env.example
pnpm db:migrate # apply migrations to your Postgres database
pnpm dev # http://localhost:3000 — sign up, create your venue
# optional sample data, scoped to an org you created (see Authentication):
SEED_ORG_ID=org_xxx pnpm db:seedIf a
pnpmscript is blocked by a pre-run check on your machine, run the underlying binary directly, e.g.node_modules/.bin/tsx prisma/seed.ts.
Uses PostgreSQL via Prisma's @prisma/adapter-pg adapter. Any Postgres works;
the project is set up against Supabase.
- Set
DATABASE_URLin.envto your connection string. For Supabase, use the Session pooler string (IPv4-compatible) from the dashboard's Connect modal — the directdb.<ref>.supabase.cohost is IPv6-only and may fail on IPv4 networks. Example:DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres" pnpm db:migrateapplies migrations;pnpm db:studioopens Prisma Studio.- The Prisma client is generated to
lib/generated/prismaand instantiated with the pg adapter in lib/prisma.ts.
Auth is handled by Clerk; each organization is one venue and all data is scoped to the active organization.
- Create an app at dashboard.clerk.com.
- Enable Email and Google sign-in, and turn on Organizations (Configure → Organizations).
- Copy your keys into
.env(see.env.example):NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..." CLERK_SECRET_KEY="sk_test_..." pnpm dev, sign up, then create your venue when prompted at/select-org.- To load sample data into that venue, copy its org id (
org_…) from the Clerk dashboard and runSEED_ORG_ID=org_xxx pnpm db:seed.
Keyless dev mode: without keys, Clerk v7 auto-provisions a temporary dev instance so you can click around immediately. It's ephemeral (resets on server restart) — set real keys for stable local development.
Auth is enforced in two layers: proxy.ts (Clerk middleware) protects every route
except /sign-in and /sign-up, and requireOrg() (lib/auth.ts)
redirects users with no active organization to /select-org and returns the
orgId/userId that every page and server action uses to scope data.
When a purchase order enters the Sent state — created as Sent, or moved Draft→Sent via "Mark as sent" — the vendor is emailed a full order summary (line items, totals, dates, notes) via Resend.
- Set
RESEND_API_KEYandORDER_EMAIL_FROMin.env(see.env.example). - Dev: the default sender
onboarding@resend.devworks without a domain but can only deliver to your own Resend account's email. Production: verify a domain in Resend and setORDER_EMAIL_FROMto an address on it. - Sending is best-effort: if the key is unset or the vendor has no email on file, the order still saves and the "Mark as sent" toast says what happened. It never blocks or fails order creation.
Logic lives in lib/email.ts (send + HTML template) and
notifyVendorOrderSent() in lib/actions/orders.ts.
| Script | What it does |
|---|---|
pnpm dev |
Start the dev server |
pnpm build |
Production build |
pnpm db:migrate |
Create/apply a migration (prisma migrate dev) |
pnpm db:reset |
Drop and recreate the DB, then re-run seed |
pnpm db:seed |
Seed sample data |
pnpm db:studio |
Open Prisma Studio to browse the data |
- Dashboard — inventory value, wines tracked, low-stock count, month-to-date spend, reorder suggestions (below par), recent orders and invoices.
- Wines — bottle inventory with type, vintage, vendor, quantity, par level, cost/sale price and a low-stock badge.
- Vendors — distributors with contact details; detail view rolls up their wines, orders and total invoiced.
- Purchase Orders — multi-line orders with a status lifecycle (Draft → Sent → Received / Cancelled). Marking an order received increments wine inventory for each line item, in a transaction.
- Invoices — vendor invoices logged as expenses with manual line items and an auto-computed total.
Vendor, Wine, PurchaseOrder + PurchaseOrderItem, Invoice + InvoiceItem.
See prisma/schema.prisma. The Prisma client is generated
to lib/generated/prisma and instantiated in lib/prisma.ts.
app/ # routes: dashboard (/), /wines, /vendors, /orders, /invoices
components/ # app-sidebar, page-header, delete-button, forms/, ui/ (shadcn)
lib/
actions/ # server actions (create/update/delete + order status transitions)
prisma.ts # PrismaClient singleton (pg adapter)
constants.ts# wine types + order statuses
utils.ts # cn(), currency/date formatters
prisma/ # schema, migrations, seed.ts