Official merchandise store for Perhimpunan Pelajar Indonesia di Jerman (PPI Jerman) — the Indonesian student association in Germany.
Customers can browse products, place orders, and submit bank transfer payment proofs. Admins review payments and manage order fulfillment through a built-in dashboard.
- Product catalog — browse items with size, color, and fit-type (Regular / Oversized) variants
- Shopping cart — persistent cart tied to signed-in user accounts
- Order placement — variant-level stock management with price snapshotted at purchase
- Payment flow — manual bank transfer upload; admins review and approve proofs
- Order tracking — customers can follow their order from payment through shipping
- Admin dashboard — separate views for payment review and order management (
ADMIN_KK/ADMIN_ITroles) - Role-based access control —
BUYER,ADMIN_KK,ADMIN_ITroles enforced server-side
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, RSC) |
| Language | TypeScript (strict mode) |
| Database | PostgreSQL via pg driver with connection pooling |
| Auth | Clerk (@clerk/nextjs) synced to local users table via webhook |
| Styling | CSS custom properties — no Tailwind or CSS-in-JS |
| Mutations | Next.js Server Actions — no separate API layer |
| Migrations | dbmate |
Resend (@react-email/components) |
|
| Shipping | SendCloud API |
- Node.js 18+
- PostgreSQL database
- Clerk account
- Resend account (transactional email)
- SendCloud account (shipping, optional for local dev)
- dbmate for migrations
git clone https://github.com/ppijerman/ppij-shop.git
cd ppij-shop
npm installCreate a .env.local file in the project root:
# Database
DATABASE_URL=postgres://user:password@localhost:5432/ppij_shop
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
CLERK_WEBHOOK_SECRET=whsec_...
# Email (Resend)
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=no-reply@shop.ppijerman.org
ADMIN_KK_EMAIL=partnership@ppijerman.org
# Shipping (SendCloud) — leave blank to disable shipping integration locally
SENDCLOUD_API_KEY=
SENDCLOUD_API_SECRET=
SENDCLOUD_SENDER_ADDRESS_ID=
SENDCLOUD_WEBHOOK_SECRET=
SENDCLOUD_TEST_MODE=true
# Cron job secret — used to authenticate the /api/cron/* endpoints
CRON_SECRET=npm run db:up # run all pending migrations
npm run db:status # check migration status
npm run db:new # create a new migration file
npm run db:rollback # roll back the last migrationnpm run dev # starts dev server at http://localhost:3000
npm run build # production build
npm run lint # lint checksrc/
├── app/ # Next.js App Router pages
│ ├── globals.css # Design tokens (CSS variables)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # / — Home
│ ├── catalog/ # /catalog — Product listing
│ ├── product/[slug]/ # /product/[slug] — Product detail
│ ├── bundle/[slug]/ # /bundle/[slug] — Bundle detail
│ ├── cart/ # /cart — Shopping cart
│ ├── account/orders/ # /account/orders — Order history & payment upload
│ └── admin/[role]/ # /admin/kk | /admin/it — Admin dashboard
│
├── lib/
│ ├── db.ts # DB pool & withTransaction()
│ ├── auth.ts # requireAdmin(), requireOrderAdmin()
│ ├── users.ts # getCurrentDbUserOrThrow()
│ ├── actions/ # Server Actions (auth-guarded mutations)
│ └── dal/ # Data Access Layer (raw SQL queries)
│
├── components/ # UI components
├── context/ # CartContext, ToastContext
└── types/index.ts # Shared TypeScript types
AWAITING_PAYMENT → PAYMENT_REVIEW → PROCESSING → SHIPPED → DONE
↘ CANCELLED
- Customer places an order → status:
AWAITING_PAYMENT - Customer uploads bank transfer proof → status:
PAYMENT_REVIEW - Admin approves payment → status:
PROCESSING - Admin marks as shipped → status:
SHIPPED - Order delivered → status:
DONE
| Role | Access |
|---|---|
BUYER |
Place orders, upload payment proofs, view own orders |
ADMIN_KK |
Review and approve payment proofs |
ADMIN_IT |
Manage orders, shipping, and product data |
Admin routes live under /admin/[role] and are enforced inside Server Actions via requireAdmin() / requireOrderAdmin().
- Branch off
master— never push directly tomaster - Open a pull request with a clear description of the change
- If your change includes a migration, include it in the same PR
- Get review and approval before merging
Internal project — PPI Jerman. Not open for public contributions at this time.