▌ gebecert.md
▒ PATH:
▒ SIZE: 9.8 KB
▒ MODIFIED: 2026-05-05 02:53
MemPalace Archive/projects/gebecert.md▒ SIZE: 9.8 KB
▒ MODIFIED: 2026-05-05 02:53
# GeBeCert Project Brief
## What is it?
B2B SaaS anti-counterfeiting platform via QR code.
"Stripe for product authentication."
Brands: subscription to generate QR codes for products
Consumers: scan QR → verify authenticity via branded web page (no login)
## Tech Stack
- Next.js 15 (App Router), TypeScript, Tailwind CSS
- Drizzle ORM + SQLite (local), PostgreSQL (prod)
- bcryptjs, jose (JWT), jszip, qrcode (QR generation), Resend (email)
## Core Flow
### Brand (B2B dashboard)
- Sign up → email verification (domain must match website) → login → dashboard
- Dashboard: manage products, generate QR codes, view analytics, consumer database
### Consumer (no login)
- Scan QR → /verify/[serial] → branded page → enter PIN if required → see result
- No account needed
## Database Schema
### companies
- id, name, email, passwordHash, plan (free/growth/pro/enterprise), website
- Brand customization: logoUrl, bannerUrl, brandColor (#1780e3), brandTextColor (#ffffff), heroText, customMessage
- Email verification: verified, verifyToken, verifyTokenExpiry
- createdAt
### users
- id, companyId (FK), email, passwordHash, name, role, createdAt
### products
- id, companyId (FK), name, sku, description, imageUrl
- Product-level branding: heroText, customMessage (fallback to company)
- createdAt
### qrCodes
- id, productId (FK), serial (unique, format: GB-XXXX-XXXX-XXXX), pinType, pin (SHA256 hashed), status, verifiedAt, consumerName, consumerEmail, createdAt
- pinType: 'none' | 'visible' | 'scratch_off' | 'one_time'
- status: 'unused' | 'verified' | 'already_registered'
### scans
- id, qrCodeId (FK), ip, city, country, device, createdAt
### teamInvites
- id, companyId (FK), email, role (admin/viewer), token (unique), invitedBy, status (pending/accepted/expired), expiresAt, acceptedAt, createdAt
## PIN Types
- **none**: No PIN — consumer scans QR, sees result immediately
- **visible**: PIN printed on label next to QR, consumer types it
- **scratch_off**: PIN hidden under scratch layer, consumer scratches then enters
- **one_time**: PIN invalidated after first verification (pin cleared from DB)
## Branding Hierarchy (Option A — product level)
Product-level overrides → falls back to company defaults:
- heroText → company.heroText → 'Verify Your Product'
- customMessage → company.customMessage → (empty)
- imageUrl (product photo used as logo) → company.logoUrl
- brandColor → company.brandColor → #1780e3
- brandTextColor → company.brandTextColor → #ffffff
- bannerUrl → company only (not per-product)
## Key Routes
### Public
- `/verify/[serial]` — consumer verify page, fully branded per company+product
- `/api/verify` — POST: verify PIN, register scan, return result
- `/verify-email?token=X` — email verification landing page
- `/check-email` — shown to unverified B2B users trying to access dashboard
### Dashboard (requires auth + email verified)
- `/dashboard` — overview
- `/dashboard/products` — product CRUD + batch CSV upload + QR count per product
- `/dashboard/generate` — QR batch generator with PIN type selection
- `/dashboard/generate?productId=X` — pre-selected product
- `/dashboard/analytics` — stats, tabs: overview / scans / products, grey market alerts
- `/dashboard/consumers` — consumer registrations list, filter, CSV export
- `/dashboard/settings` — company + brand customization with live preview
- `/dashboard/team` — team members, invite form, pending invites, role management
- `/invite/[token]` — invite acceptance page (create password, join team)
- `/login` — B2B login
- `/signup` — B2B signup with domain check
### API
- `POST /api/products/batch` — CSV upload: serial,product_name,sku,pin_type → creates QR codes. Auto-creates products. Max 10,000 rows.
- `POST /api/products/[id]/generate` — generates quantity QR codes, returns ZIP (PNG QR images + codes.csv + README.txt)
- `GET /api/companies/me` — returns company with brand fields
- `PATCH /api/companies/[id]` — updates company + brand fields
- `GET /api/products` — list products with QR count
- `POST /api/products` — create product
- `PATCH /api/products/[id]` — update product (including branding fields)
- `POST /api/auth/signup` — domain check (email domain = website domain)
- `POST /api/auth/login` — login, sets cookie
- `POST /api/auth/logout` — logout, clears cookie
- `GET /api/verify-email?token=X` — validates token, marks verified, redirects to /login
- `POST /api/auth/resend-verification` — resend verification email
- `GET /api/analytics` — all analytics data (stats, scans by country, top products, recent registrations)
- `GET /api/me` — current user + company session data
- `GET /api/team` — list members + pending invites
- `POST /api/team` — invite member (admin only), returns invite URL
- `PUT /api/team/accept` — accept invite, create account + auto-login
- `PATCH /api/team/manage` — update member role
- `DELETE /api/team/manage` — remove member or cancel invite
## Batch CSV Format
```
serial, product_name, sku, pin_type
GB-ABCD-EFGH-IJKL, Jordan 1 Retro, JRD-001, scratch_off
GB-0000-0001-0002, Air Max 90, AMX-002, visible
```
- Auto-creates product if not found
- Max 10,000 rows per upload
- Duplicates blocked (by serial)
## QR Generation Output (ZIP)
- `[serial].png` — QR code image (400×400px, high error correction)
- `codes.csv` — Serial,PIN,Verification URL (PIN excluded if pinType=none)
- `README.txt` — instructions and PIN type guide
## Email Verification (B2B)
- Signup: domain must match website (anti-scam measure)
- Token sent via Resend API (free tier sufficient for low B2B volume)
- Dashboard blocked until verified → redirects to /check-email page
- Resend verification link available on /check-email page
- .env.local needs: RESEND_API_KEY, NEXT_PUBLIC_BASE_URL
## What's Built
✅ Consumer verify page — fully branded per company+product (Option A)
✅ Product-level branding overrides (heroText, customMessage, imageUrl)
✅ Company-level brand customization (logo, banner, colors, hero, message)
✅ Brand settings UI — live preview of verify page
✅ Products CRUD — with branding fields, QR count display
✅ Batch CSV upload — Excel workflow, auto product creation
✅ QR batch generator — ZIP with PNGs + CSV + README, PIN type selection
✅ PIN types — none/visible/scratch_off/one_time
✅ Email verification — domain check, Resend integration, resend flow
✅ Dashboard layout — auth gate + email verification gate
✅ Analytics dashboard — 5 stat cards, 3 tabs, grey market alert banner
✅ Consumer database — registrations list, filter by email/anonymous, CSV export
✅ Consumer verify API — PIN verification, one-time PIN invalidation, scan logging
✅ Team management — invite by email, role selection (admin/viewer), accept invite flow, remove members, change roles
## Security Hardening (May 5, 2026)
OpenCode code review via subagent + OpenRouter free model → 3 CRITICAL, 6 HIGH, 6 MED issues found and fixed:
### Fixed
- `auth.ts`: JWT_SECRET throws error if env var missing (was silent fallback) — REQUIRES `JWT_SECRET` in `.env.local`
- `auth.ts`: `generateSerial()` + `generatePin()` now use `crypto.randomBytes()` / `crypto.randomInt()` instead of `Math.random()`
- `auth.ts`: `generatePin(length)` now accepts length param (was hardcoded 4-digit)
- `verify/route.ts`: PIN comparison uses `crypto.timingSafeEqual()` (was plain `!==` — timing attack vector)
- `verify/route.ts`: Failed PIN attempts no longer write to scan log (was polluting analytics)
- `settings/page.tsx`: `safeUrl()` helper strips `javascript:`/`data:`/non-http protocols from logoUrl/bannerUrl before use in `backgroundImage` and `<img src>`
- `VerifyClient.tsx`: PIN form conditional on `needsPin` (was always rendering, misleading UX for `pinType=none`)
- `VerifyClient.tsx`: Button min-length now `pinMinLength` (6 for scratch_off/one_time, 4 for visible) — was hardcoded 4
### Known/Accepted
- No brute-force rate limiting on PIN attempts (medium priority — add IP-based rate limit middleware in future)
- `x-forwarded-for` treated as untrusted (geo-data can be spoofed — scan analytics noted as best-effort)
- `as any` casts in Drizzle inserts (pre-existing, schema mismatch — refactor to proper types later)
- `already_registered` status not in enum constraint (data migration needed — low risk)
## What's Next (priority order)
1. ~~Team management~~ ✅
2. WhatsApp bot — TBD (later phase)
3. Product image upload — S3/Cloudinary instead of URL field
4. Overview/dashboard home page — summary cards as landing after login
5. Invites sent via email (currently logs URL to console — needs RESEND_API_KEY)
## Notes
- Low B2B volume → Resend free tier is sufficient
- WhatsApp: shared bot number vs per-company API — TBD, deprioritized
- Brand customization is CRITICAL for consumer trust on verify page
- Consumers never login — public verify page only
- Serial format: GB-XXXX-XXXX-XXXX (3 groups of 4 chars, uppercase)
- PIN: 4-digit numeric, SHA256 hashed in DB
- scan logging captures ip, city, country, device for geo analytics
- Grey market detection: 'already_registered' status set when a one-time PIN is reused
## External APIs
- **OpenRouter:** `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` — free tier, all free models
- Vision models (verified May 2026): `nvidia/nemotron-nano-12b-v2-vl:free` (detailed), `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free` (concise)
- Vision skill: `~/.hermes/skills/autonomous-ai-agents/vision-subagent/`
- Local Telegram images: `/Users/nick/.hermes/image_cache/img_XXXX.jpg` → base64 encode → OpenRouter
- **Resend:** needs `RESEND_API_KEY` in `.env.local` for email to actually send
- **JWT_SECRET:** must be set in `.env.local` (app throws error if missing)
- **Best free coding model:** `google/gemma-4-26b-it:free` (262K context)
- **Best free long-doc model:** `openrouter/owl-alpha` (1M context)