A Next.js 14 SaaS that combines digital wedding/event invitations with QR-code guest check-in, souvenir tracking, and a hybrid offline-first sync mode. Built as a final project for the E-Business course at Universitas Multimedia Nusantara.
- Digital invitation page per couple/event at
/i/[slug](14 templates with different aesthetics & animations). - Guest list management with WhatsApp share helper (
wa.me/?text=link, no API). - QR check-in scanner with three input modes (camera, photo upload, manual paste).
- Souvenir tracking mode β same scanner, toggle between check-in and souvenir distribution. Server enforces "must check-in before souvenir" and prevents double-claim.
- Hybrid offline-first sync β operator can scan to local MySQL during the event, then push to TiDB Cloud after WiFi is back.
- Midtrans Snap payment integration (sandbox toggle via env) for template purchases and per-event service plans.
- Not a finished product. This is an academic project built within ~3 weeks.
- Not load-tested. Numbers below are estimates from free-tier limits, not benchmarks.
- Not a replacement for an EO's full operational stack β there's no email automation, no real-time push, no recurring billing yet.
- Souvenir tracking appears uncommon in Indonesian digital-invitation tools we surveyed; we did not exhaustively verify the entire market.
- Tech Stack
- Architecture
- Quick Start
- Demo Accounts
- Project Structure
- API Reference
- Database Schema
- Security Notes
- Pricing
- Known Limits & Roadmap
- Documentation
- License
| Layer | Choice |
|---|---|
| Framework | Next.js 14.2 (App Router) + React 18 + TypeScript 5 |
| Database | TiDB Cloud (production), MySQL 8 / XAMPP (local dev) |
| ORM | Prisma 5.14 (relationMode: prisma) |
| Auth | bcryptjs (cost 10) + httpOnly cookie session |
| Storage | Cloudinary (free tier, auto webp/avif) |
| Payments | Midtrans Snap (sandbox + production) |
| QR Scanner | html5-qrcode (camera) + jsqr (photo) |
| Animation | Framer Motion 12 + anime.js 4 |
| Hosting | Vercel free tier |
| Decision | Reason |
|---|---|
| TiDB Cloud over PlanetScale | Free 5GB tier, MySQL-compatible, no shard lock-in. |
| Cloudinary over R2 | Free 25GB and on-the-fly transform β saves CDN config work for an MVP. |
| bcryptjs + cookies over NextAuth | Single-tenant SaaS doesn't need OAuth provider plumbing yet. |
| UUID v4 over AES-256 for QR | Token validated by DB lookup, not by client decode β no shared key to leak. |
| Manual sync over WebSocket | The bottleneck we observed is venue connectivity, not message latency. |
Vercel (Next.js 14 App Router)
β
ββββββββββββββββββββββββΌβββββββββββββββββββββββ
β β β
Public routes API routes Admin routes
/, /catalog, /api/checkin /admin/dashboard
/i/[slug], /api/sync/push /admin/scanner
/inv/[token] /api/payment/midtrans /admin/users
β
βΌ
βββββββββββββββββββββββββββββββββ
β TiDB Cloud (MySQL-compatible)β
β User Β· Event Β· Guest β
β Attendance Β· ChangeLog β
βββββββββββββββββββββββββββββββββ
β² β²
β β
Cloudinary CDN XAMPP MySQL (local)
(image upload) (offline scanner mode)
Venue (no internet) Post-event (WiFi)
βββββββββββββββββ βββββββββββββββββ
Laptop + XAMPP POST /api/sync/push
MySQL local ββββββΊ 1. Read local guests
Scanner posts to 2. Open TiDB connection
localhost 3. UPSERT by token (UUID = idempotent)
4. Forward souvenir flags
Token-based UPSERT means re-running the sync is safe: each guest has a globally-unique UUID, and the merge resolves to the same row.
- Node.js β₯ 18
- TiDB Cloud account (free)
- Cloudinary account (free)
- Midtrans Sandbox account (free)
- Optional: XAMPP / MySQL 8 on
localhost:3306for offline-first mode
git clone https://github.com/GodrezJr2/Lumina-Card.git
cd Lumina-Card
npm install
cp .env.example .env
# fill in DATABASE_URL, MIDTRANS_*, CLOUDINARY_*
npx prisma generate
npx prisma db push
node scripts/seed-demo.mjs # 5 users + 2 events + 13 guests
npm run dev # http://localhost:3001DATABASE_URL="mysql://user:pass@host:4000/db?sslaccept=strict"
PROD_DATABASE_URL="..." # only needed for offline-first sync
MIDTRANS_SERVER_KEY="SB-Mid-server-..."
NEXT_PUBLIC_MIDTRANS_CLIENT_KEY="SB-Mid-client-..."
MIDTRANS_IS_PRODUCTION="false"
CLOUDINARY_CLOUD_NAME="..."
CLOUDINARY_API_KEY="..."
CLOUDINARY_API_SECRET="..."For local development, use the seed scripts (excluded from this repo for credential hygiene). After running them, accounts cover all five RBAC tiers: SUPER_ADMIN, FULL_SERVICE_CLIENT, DIY_CLIENT, USHER_STAFF, and BASIC_USER.
Live deployment credentials are not published. Browse the public invitation pages above for a read-only view.
wedding-app/
βββ app/
β βββ (public routes: /, /catalog, /pricing, /i/[slug], /inv/[token]/qr)
β βββ admin/ β dashboard, events, guests, scanner, broadcast, users, panel
β βββ api/ β auth, events, guests, checkin, sync, upload, payment/midtrans
βββ components/
β βββ templates/ β 14 invitation templates
β βββ ImageUpload.tsx
β βββ MusicPlayer.tsx
β βββ RoleGate.tsx
βββ lib/
β βββ prisma.ts β production client (TiDB)
β βββ prisma-local.ts β local client (XAMPP, for sync)
β βββ cloudinary.ts
β βββ roles.ts β 5-tier RBAC permission map
β βββ catalog-templates.ts
βββ prisma/schema.prisma β User Β· Event Β· Guest Β· Attendance Β· ChangeLog
βββ scripts/
βββ seed-demo.mjs β reset + seed (idempotent)
βββ seed-passwords.mjs β reset all passwords to "123456"
βββ list-users.mjs β CLI table dump
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/login |
Sets user_id + user_role cookies |
POST |
/api/auth/logout |
Clear session |
GET |
/api/auth/me |
Current user |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/events |
List events + aggregate stats |
POST |
/api/events |
Create event with auto-generated slug |
GET |
/api/guests?eventId={id} |
Guests with attendance |
POST |
/api/guests/bulk |
Bulk import (pipe / tab / comma separators) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/checkin |
{ token, mode: "checkin" | "souvenir" } |
POST |
/api/sync/push |
Push local DB to TiDB Cloud (idempotent UPSERT) |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/payment/midtrans/create |
Snap transaction (template OR plan) |
POST |
/api/payment/midtrans/notification |
Webhook with SHA512 verify |
POST /api/checkin
{ "token": "550e8400-e29b-...", "mode": "souvenir" }Success β 200:
{ "name": "Citra", "status": "Checked_In", "mode": "souvenir",
"pickedUpSouvenir": true, "message": "Souvenir berhasil diberikan!" }Not yet checked in β 409:
{ "name": "Dewi", "status": "Opened",
"message": "Tamu belum check-in. Wajib check-in dulu sebelum ambil souvenir." }User 1 βββββ N Event
Event 1 βββββ N Guest
Guest 1 βββββ 1 Attendance
User 1 βββββ N ChangeLog (audit)
Draft ββsend WAβββΊ Sent ββopen linkβββΊ Opened ββscan QRβββΊ Checked_In
β
scan in βΌ
souvenir Souvenir picked
mode
Schema details: see prisma/schema.prisma.
| Area | Implementation |
|---|---|
| Transport | HTTPS via Vercel + Let's Encrypt |
| DB | TiDB Cloud ?sslaccept=strict |
| Password | bcryptjs cost 10 |
| Session | httpOnly cookie, 7-day TTL |
| Authorization | 5-tier RBAC, route-level <RoleGate> |
| QR token | UUID v4 (122-bit), validated against DB |
| Webhook | Midtrans SHA512 signature verify |
| Audit log | ChangeLog table records actor + before/after JSON |
| Soft delete | User.deletedAt, restorable by SUPER_ADMIN |
| Card data | None stored β Midtrans handles tokenization |
We don't claim formal compliance certifications. Card data flows entirely through Midtrans, which itself is PCI-DSS Level 1.
PER-EVENT SERVICE PLANS
β Basic Rp 79.000 200 guests, 1 staff
β Professional Rp 199.000 1.000 guests, unlimited staff, WA blast
β Enterprise custom white-label, API, SLA
TEMPLATE CATALOG (one-time)
β 14 templates Rp 149.000 β Rp 399.000
Operational costs at MVP scale: ~Rp 0/month (everything on free tiers) plus ~Rp 192K/year for the domain. Midtrans fees (2.9β3.5% per transaction) are passed through.
- No real-time dashboard updates β admin pages re-fetch on navigation, not live push.
- Bulk import is textarea paste only (CSV file upload is on the roadmap).
- No 2FA on SUPER_ADMIN accounts.
- WhatsApp distribution is a
wa.melink generator β no automated blast. - Recurring subscription billing not implemented (everything is pay-per-event).
- RSVP response tracking (Hadir/Tidak/Mungkin + plus-ones)
- Live scanner stats counter
- CSV file upload with header detection
- Mobile drawer nav for admin
- WhatsApp blast via baileys (self-hosted)
- EO subscription billing
- 2FA for SUPER_ADMIN
DEMO_FLOW.mdβ 5-scenario presentation script (12-15 min)LAPORAN_REVISI.mdβ Final report β tech stack overviewLAPORAN_REVISI_BAB2.mdβ InfrastructureLAPORAN_REVISI_BAB5_6.mdβ Security & monetizationLAPORAN_REVISI_NEW_SECTIONS.mdβ Souvenir tracking design
MIT. Fork it, learn from it, adapt it.