An AI-powered bilingual cocktail experience that turns a quick mood check into a complete drink recommendation.
MoodShaker is a bilingual web app for cocktail discovery and recommendation. Instead of browsing a static recipe database first, users answer a short mood-based questionnaire and receive a personalized cocktail with ingredients, tools, steps, and a shareable visual card.
The project is built with Next.js App Router, React 19, TypeScript, Prisma, PostgreSQL, and a pair of AI-backed endpoints for recommendation and image generation. It also ships with a localized experience for Chinese and English users, a gallery for browsing drinks, and a detail page for revisiting recipes.
- Batch 1 hardening is complete: local verification is restored, private recommendation access no longer puts
editTokenin the URL, unsafe wildcard CORS headers were removed, API 500s were standardized, and shared rate-limit storage was added. - Lightweight automated verification is now part of the repo through
pnpm test. - Recommendation recovery is intentionally same-browser-session access. When local edit access is unavailable, the product now shows an explicit unavailable state instead of a silent failure.
- Suitable for local development, staging, and controlled beta testing.
- Not yet recommended for a full production launch.
Formal release blockers are tracked in docs/release-readiness.md.
- Two recommendation styles are supported through different bartender personas:
classic_bartenderandcreative_bartender. - Language-aware routing is built in, with
/cnand/enpaths plus automatic redirect logic in proxy.ts. - The main product flow is connected: landing page, questionnaire, recommendation page, gallery, cocktail details, and share card output.
- Recommendation access is private by default and requires both the recommendation id and a local edit token.
- Server responses use stable error codes and generic client-safe 500 messages.
- Recommendation and image endpoints emit rate-limit headers and can use shared Postgres-backed buckets.
| Home | Questionnaire |
|---|---|
![]() |
![]() |
| Gallery | Cocktail Detail |
|---|---|
![]() |
![]() |
Landing page
-> mood questionnaire
-> AI cocktail recommendation
-> generated image + recipe details
-> shareable card
-> gallery browsing
-> cocktail detail revisit
- Framework: Next.js 16 with App Router
- UI: React 19, Tailwind CSS 4, Framer Motion, Radix UI, Lucide
- Language: TypeScript
- Data layer: Prisma with PostgreSQL
- Data fetching: SWR
- AI integration: OpenAI-compatible chat endpoint and image generation endpoint
- Tooling: pnpm, ESLint, tsx, Docker Compose
flowchart LR
A["User"] --> B["Localized app routes (/cn, /en)"]
B --> C["Questionnaire / Gallery / Detail pages"]
C --> D["App API routes"]
D --> E["Cocktail recommendation provider"]
D --> F["Image generation provider"]
D --> G["Prisma Client"]
G --> H["PostgreSQL"]
C --> I["Client persistence / share card / motion UI"]
- App routes live under app/[lang] and provide the user-facing pages.
- API handlers live under app/api and coordinate recommendation generation, detail lookup, image generation, and private recommendation retrieval.
- Shared UI lives in components, while app-wide state is managed in context.
- Database schema, migrations, seed data, and maintenance scripts are in prisma.
- Current remediation notes and performance tracking live under docs.
app/
api/
cocktail/
image/
recommendation/
[lang]/
page.tsx
questions/page.tsx
gallery/page.tsx
cocktail/[id]/page.tsx
cocktail/recommendation/page.tsx
components/
animations/
layout/
pages/
share/
ui/
context/
docs/
plans/
screenshots/
lib/
locales/
prisma/
public/
tests/
proxy.ts
- Node.js
>= 22 - pnpm
>= 10 - PostgreSQL
15+or Docker
pnpm installpostinstall runs prisma generate, so a clean install should produce a usable Prisma client automatically.
cp .env.example .envThen fill in the required values in .env.
Make sure PostgreSQL is running and DATABASE_URL is reachable:
pnpm db:initThis command will:
- generate the Prisma client
- apply migrations
- seed initial cocktail data
pnpm devOpen http://localhost:3000. Requests to / are redirected to the proper language path, defaulting to /cn.
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | API key for the chat recommendation endpoint |
OPENAI_BASE_URL |
Yes | OpenAI-compatible base URL |
OPENAI_MODEL |
Yes | Chat model name |
IMAGE_API_URL |
Yes for image generation | Image generation endpoint |
IMAGE_API_KEY |
Yes for image generation | Image generation API key |
IMAGE_MODEL |
No | Image model name |
IMAGE_FETCH_HOST_ALLOWLIST |
Recommended | Comma-separated host allowlist for server-side image fetch and optimization |
DATABASE_URL |
Yes | PostgreSQL connection string |
HOST_PORT |
Optional | Exposed web port for Docker Compose |
POSTGRES_USER |
Optional | Database username for Docker Compose |
POSTGRES_PASSWORD |
Optional | Database password for Docker Compose |
POSTGRES_DB |
Optional | Database name for Docker Compose |
| Command | Description |
|---|---|
pnpm dev |
Start the local development server |
pnpm build |
Build the production bundle |
pnpm start |
Run the production build |
pnpm lint |
Run ESLint |
pnpm test |
Run the lightweight Node-based regression suite |
pnpm test:e2e |
Run the Playwright smoke test for the localized home -> questions flow |
pnpm db:init |
Generate Prisma client, apply migrations, and seed data |
pnpm prisma:generate |
Generate Prisma client only |
pnpm prisma:migrate |
Apply Prisma migrations |
pnpm prisma:seed |
Seed cocktail data |
pnpm prisma:backfill-thumbnails |
Backfill the thumbnail field from stored images |
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/cocktail |
Generate a cocktail recommendation from questionnaire input |
GET |
/api/cocktail/:id |
Fetch a public cocktail detail record by id |
POST |
/api/image |
Generate or refresh a recommendation image when the caller has edit access |
POST |
/api/recommendation/:id |
Retrieve a private recommendation by id using an editToken in the JSON body |
editTokenis no longer accepted in the URL for private recommendation access.POST /api/recommendation/:idreturns recommendation metadata without echoing theeditToken.POST /api/cocktailis currently rate-limited by client IP.POST /api/imageis rate-limited per recommendation id.- In production, missing shared rate-limit storage is treated as a deployment error instead of silently falling back forever.
- Supported languages are
cnanden. - proxy.ts detects language from the URL, cookie, and
Accept-Languageheader. - Requests without a language prefix are redirected to a localized route.
- Translation dictionaries live in locales/cn.ts and locales/en.ts.
This repository already includes the essentials for containerized deployment:
- Dockerfile for multi-stage image builds
- docker-compose.yml for the web app and PostgreSQL
- scripts/docker-entrypoint.sh for startup-time schema initialization and seed handling
- Set all required environment variables.
- Run Prisma migrations against the target database.
- Verify the
rate_limit_bucketstable exists after deploy. - Verify the recommendation endpoint returns
503instead of a generic500when shared limiter storage or the primary database is unavailable. - Run the verification commands:
pnpm test
pnpm test:e2e
pnpm lint
pnpm build- Run a manual smoke pass:
- home -> questions -> recommendation
- image generation / refresh
- recommendation restore from the same browser session
- gallery search and filter
- cocktail detail page in both languages
The project is not yet ready for a full GA release. See docs/release-readiness.md before promoting beyond staging or a controlled beta.
If you see:
The column `cocktails.thumbnail` does not exist in the current database
Run:
pnpm db:initIf the database already exists and only migrations are missing, you can also try:
pnpm prisma:migrateIf recommendation or image requests start failing in production after the hardening batch, verify that the latest Prisma migration was applied and that the rate_limit_buckets table exists.
- Check that the keys and URLs in
.envare correct. - Make sure
OPENAI_BASE_URLpoints to an OpenAI-compatible endpoint. - Confirm
IMAGE_FETCH_HOST_ALLOWLISTincludes any remote host you expect the server to fetch for image optimization. - Inspect the server logs for the handlers under app/api.
Private recommendation recovery is currently same-browser-session access. If local edit access was cleared, the app will show an explicit unavailable state and ask the user to generate a new recommendation.
The baseline checks for this repository are:
pnpm test
pnpm lint
pnpm buildIf you install dependencies in an environment where Prisma client generation was skipped, run this once before pnpm build:
pnpm prisma:generateRecommended manual smoke checks:
- Complete the questionnaire and verify recommendation generation.
- Refresh the recommendation image and confirm rate-limit feedback behaves correctly.
- Reopen a private recommendation from the same browser session.
- Confirm a private recommendation without local access shows the unavailable state.
- Browse the gallery and confirm search or filters still behave correctly.
- Open a cocktail detail page and switch languages.
- Verify share card generation and download flow if you touch that area.
- docs/release-readiness.md: current staging vs production readiness
- docs/performance-baseline.md: local verification baseline and known bottlenecks
- docs/plans/2026-04-07-moodshaker-remediation-implementation-plan.md: remediation plan and current phase status
If you open a PR, include:
- a short summary of the change
- linked issue or context, if any
- verification steps
- screenshots for UI changes
- notes for environment or database updates
- AI-generated content should be reviewed before real-world use.
- Do not commit
.envor any production secret values. - Current automated coverage is still lightweight. Treat
pnpm testas regression support, not a substitute for end-to-end verification.



