Product: Frame-It-Now — mobile-first selfie PWA for events and public engagement.
Repository / UI strings: The codebase and some pages still label the app “Camera”; operationally this is the same product.
Version: 2.9.0 (canonical: package.json → "version")
Last Updated: 2026-04-09
Status: Production-ready
A Next.js application: users capture photos, apply branding frames, upload to a CDN, store metadata in MongoDB, share via public links, and display submissions on event slideshows with fair rotation and aspect-aware layouts.
npm install
npm run dev # http://localhost:3000
npm run build
npm start
| Area | Choice |
|---|---|
| Framework | Next.js (App Router) |
| Language | TypeScript (strict) |
| Database | MongoDB |
| Auth | SSO (OAuth2/OIDC + PKCE), encrypted session cookie |
| Image hosting | imgbb.com |
| Styling | Tailwind CSS |
| Transactional email | Not wired (SSO does not send app mail; see docs/AUTHORIZATION.md) |
/capture/[eventId] (event) or /capture (legacy global capture).getUserMedia) or file upload (legacy page)./api/submissions: sends base64 image data + event UUID + optional userInfo / consents./share/[id] uses the submission’s MongoDB _id and imageUrl for OG tags and display./slideshow/[slideshowId] fetches playlist JSON, renders 16:9 single or mosaic slides, POSTs play counts so least-played items surface more often.| Layer | Responsibility |
|---|---|
| Browser | Camera, canvas compositing, share/download UX, slideshow player (FIFO queue + preload, see docs/SLIDESHOW_LOGIC.md). |
| Next.js API routes | Events, frames, logos, submissions, slideshows, auth, admin CRUD. |
| MongoDB | Partners, events, frames, logos, submissions, slideshows; optional user cache; SSO DB reads for inactive-user filtering in playlists. |
| imgbb | Stores composed and admin-uploaded rasters; returns public URLs and delete URLs. |
Video → canvas snapshot (JPEG, frame aspect) → second canvas (photo + frame bitmap) → JSON POST → imgbb → MongoDB insert → slideshow aggregate (match by event UUID, sort by playCount / createdAt) → generatePlaylist → client display → /played increments counts.
components/camera/CameraCapture.tsx)getUserMedia with high ideal resolution; front/back switch; Safari-oriented readiness (metadata, canplay, delays, double requestAnimationFrame); crop to target aspect ratio; front-camera mirror fix on draw; black-frame retry.CameraCapture can show a full-bleed <img> overlay when frameOverlay is set (frame URL from CDN)./capture/[eventId]): frameOverlay is intentionally unset (avoids canvas/CORS issues); alignment is aspect-ratio viewport + WYSIWYG crop, not a live SVG mask./capture: passes frameOverlay={selectedFrame.imageUrl} so users see the frame while framing.app/capture/[eventId]/page.tsx, legacy app/capture/page.tsx)crossOrigin = 'anonymous'), draw photo then frame; frameless events crop toward 16:9 and downscale.errorFrameMessage).lib/imgbb/upload.ts, POST /api/submissions)imageUrl (and related fields) stored for CDN delivery and slideshows.optionalAuth allows userId / userEmail defaults like anonymous when no SSO session.lib/slideshow/playlist.ts, app/slideshow/[slideshowId]/page.tsx, APIs under /api/slideshows/...)GET …/playlist with optional ?limit=1 prefetch in loop mode; image preload; fire-and-forget play-count updates. Server supports exclude on playlist for other clients; details in docs/SLIDESHOW_LOGIC.md.take-photo → onboarding (who-are-you, accept, CTA).?resume=true&page=N: session fetch may pre-fill userInfo and advance the page index./slideshow/[slideshowId] → SlideshowPlayerCore loads settings + playlist → timed advance → POST /played on visible slide. Composite videowall: /slideshow-layout/[layoutId]./admin/*: gated by root middleware.ts (session cookie, appRole admin/superadmin, appAccess !== false) and app/admin/layout.tsx. Partners, events, frames, logos, slideshows, slideshow layouts, custom pages. /api/admin/* uses requireAdmin() in route handlers (also enforces appAccess). Changes apply to new sessions; open capture clients keep prior fetched config until reload.frameId on submit (404), clipboard failures for share link.| Field / pattern | Role |
|---|---|
imageUrl |
Slideshow, share page, Open Graph — required for display. |
eventId (UUID) or eventIds[] |
Playlist filter — must match the event’s eventId UUID (slideshow API resolves slideshow → event document). |
metadata.finalWidth / finalHeight |
Aspect detection; missing values fall back to 1920×1080 in playlist code and can mis-classify aspect ratio. |
playCount, createdAt |
Fair rotation (least played, then oldest). |
isArchived, hiddenFromEvents |
Excluded from slideshow when set. |
userEmail / userId |
Slideshow may filter out inactive SSO users; anonymous path must remain valid. |
Optional: userInfo, consents, deleteUrl, slideshowPlays, partner fields |
GDPR, analytics, per-slideshow play stats. |
lib/db/schemas.ts describes a rich Submission shape (e.g. submissionId, originalImageUrl, finalImageUrl, eventIds). POST /api/submissions currently persists a different document shape (imageUrl, singular eventId, userName, etc.). Slideshow and playlist code often accept imageUrl || finalImageUrl. Treat schema drift as operational risk: new code should align types, persistence, and consumers or keep explicit compatibility shims.
lib/api/rateLimiter.ts + RATE_LIMITS; checkRateLimit is used on POST /api/submissions, auth login, hashtags, event GET, slideshow routes, etc. With UPSTASH_REDIS_* env vars, limits are shared across Vercel instances; otherwise buckets are in-memory per instance.| Area | Detection | Mitigation / UX |
|---|---|---|
| Camera | API errors, black-frame check | Messages, retry, switch camera |
| Compositing | Thrown errors | User alert |
| imgbb / network | Axios errors, timeouts | Retries where configured; user retry save |
| MongoDB | Route errors | withErrorHandler → 5xx |
| Empty slideshow | Empty playlist | “No submissions yet” UI |
| Play count API | Non-OK response | Logged; playback continues |
Inactive-user filter (getInactiveUserEmails) |
DB/SSO failure | Playlist route may 500 — see code paths |
| Scale | Notes |
|---|---|
| Low tens | Typical single-instance + Mongo + imgbb is fine. |
| ~100 concurrent | imgbb quotas, Mongo write rate, playlist aggregate cost, CDN egress become visible. |
| 1k+ | Unbounded aggregation per playlist request is a memory/CPU hotspot; frequent playlist?limit=1 prefetch adds read load. |
| 10k+ | imgbb as single vendor, write amplification on /played, lack of edge caching on API reads — likely need object storage + CDN, bounded queries, Redis (limits, sessions, queues), and observability. |
app/, components/), lib/ (db, imgbb, slideshow, auth), centralized withErrorHandler, playlist logic isolated in lib/slideshow/playlist.ts.useState branches); API response shapes sometimes support both wrapped and flat payloads defensively.console.log in slideshow/playlist/imgbb paths; no structured logging or metrics in-repo — plan for production tracing.checkRateLimit usage per route./share/[id] are world-fetchable if the id is known; OG tags expose the same URL.userInfo and consents on submissions — align with retention, export, and deletion policy.IMGBB_API_KEY, Mongo URI, SSO — environment-only; imgbb delete URLs are sensitive if logged or leaked.Short-term: Reconcile submission schema vs DB writes; reduce production console noise where still noisy; document event id semantics (Mongo _id on slideshow document vs UUID on submissions).
Mid-term: Cap or paginate playlist sourcing; optional original + final image storage; unify legacy vs event capture behavior where product allows; moderation / hold queue before slideshow.
Long-term: First-party object storage + CDN + signed URLs; multi-tenant quotas; real-time slideshow channel if required; formal privacy tooling.
├── app/
│ ├── api/ # REST handlers
│ ├── admin/ # Admin UI
│ ├── capture/ # /capture + /capture/[eventId]
│ ├── slideshow/[slideshowId]/ # Fullscreen player (wraps SlideshowPlayerCore)
│ ├── slideshow-layout/[layoutId]/ # Composite layout player
│ ├── share/[id]/ # Public share + metadata
│ └── users/[name]/ # Public user profile (admin actions when permitted)
├── middleware.ts # Edge: /admin/* gate (cookie + appRole + appAccess)
├── components/
│ ├── camera/ # CameraCapture, FileUpload
│ ├── capture/ # Custom page steps
│ ├── shared/
│ └── admin/
├── lib/
│ ├── admin/ # Shared admin helpers (e.g. user-management props)
│ ├── api/ # withErrorHandler, responses, rateLimiter
│ ├── auth/
│ ├── db/ # mongodb, schemas, sso helpers
│ ├── imgbb/
│ ├── security/
│ └── slideshow/ # playlist generation, viewport-scale
├── ARCHITECTURE.md # Deeper architecture (repo root)
├── TECH_STACK.md
├── NAMING_GUIDE.md
└── docs/ # SLIDESHOW_LOGIC.md, MONGODB_CONVENTIONS.md, …
| Doc | Purpose |
|---|---|
| README.md | This file — product + system model + ops |
| ARCHITECTURE.md | Deeper architecture |
| TECH_STACK.md | Technology decisions |
| NAMING_GUIDE.md | Conventions |
| docs/SLIDESHOW_LOGIC.md | Slideshow behavior detail |
| docs/DOCUMENTATION.md | How to keep docs aligned with code |
| docs/MONGODB_CONVENTIONS.md | DB patterns |
| docs/MONGODB_ATLAS.md | Atlas setup, npm run db:verify-uri, npm run db:ensure-indexes |
| RELEASE_NOTES.md | Changelog |
| TASKLIST.md / ROADMAP.md | Planning |
Auth: GET /api/auth/login?provider=google|facebook (optional), GET /api/auth/callback, GET or POST /api/auth/logout, GET /api/auth/session
Core: partners, events, frames, logos, submissions (GET authenticated list; POST create), slideshows (.../playlist, .../played, .../background-image, …), slideshow-layouts (GET public by layoutId; admin CRUD with auth)
See ARCHITECTURE.md and route files under app/api/ for the full set.
lib/api helpers and withErrorHandler for new routes.NAMING_GUIDE.md.npm run build passes; secrets only in env; avoid logging delete URLs or PII.Copy .env.example to .env / .env.local and fill in values. Check DNS with npm run db:verify-uri; npm run env:verify exercises Mongo, SSO discovery, ImgBB, and Upstash Redis when configured.
MONGODB_URI=mongodb+srv://...
MONGODB_DB=camera
SSO_BASE_URL=https://...
SSO_CLIENT_ID=...
IMGBB_API_KEY=...
# Public base URL for this deployment (emails, links — set per host in Vercel if needed).
NEXT_PUBLIC_APP_URL=https://camera.doneisbetter.com
Production web entrypoints include https://camera.doneisbetter.com and https://camera.messmass.com. OAuth redirect_uri is derived from the browser’s host (…/api/auth/callback), so in SSO (e.g. sso.doneisbetter.com) your Camera OAuth client must allowlist every callback you use (localhost, both camera hosts, preview URLs if applicable) and you should remove retired hosts (for example an old fancam-style URL) from that client’s redirect list so tokens are not issued back to stale domains.
API rate limits use Upstash when both variables are set; otherwise limits are per serverless instance only.
UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN..env.local, then run npm run env:verify — you should see ✓ Upstash Redis: PING ok.Install/use the CLI with npx vercel@latest. Log in once: vercel login.
Link this repo to the production project (team narimato, project camera):
npx vercel@latest link --yes --scope narimato --project camera
Creates .vercel/ (listed in .gitignore — do not commit).
Pull cloud env vars into .env.local (overwrites that file — back it up first if needed):
npm run vercel:env-pull
Shorthand scripts: npm run vercel:link, npm run vercel:env-pull.
Proprietary — all rights reserved.
SSO · MongoDB Atlas · imgbb