▌ NICK COMMAND BASE ▐
2026-06-07 04:35

▌ gebecert.md

▒ PATH: MemPalace Archive/projects/gebecert.md
▒ SIZE: 9.8 KB
▒ MODIFIED: 2026-05-05 02:53
← BACK TO VAULT
# 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)
▒▒▒ READY CPU: 12% MEM: 4.2G NET: OK OBSIDIAN ▒ VIEWING