A full-stack Next.js application for tracking restaurant cooking oils and assessing peanut allergy risk. The app leverages community verification and rate limiting to ensure data accuracy and prevent abuse.
- Features
- Tech Stack
- Project Structure
- Getting Started
- Docker
- API Reference
- Testing
- Security Features
- Scripts
- Deployment
- Roadmap
- Contributing
- License
- Submit Restaurants: Add restaurants with their cooking oil type
- Real-Time Search: Debounced search with instant results and risk filtering
- Risk Assessment: Color-coded badges (High/Medium/Low/Unknown) based on oil type
- Data Persistence: MongoDB-backed storage with data that survives restarts
- Crowdsourced Verification: Restaurants require 2 matching submissions from different IPs to be marked as "Community Verified"
- Rate Limiting: Maximum 5 submissions per IP per hour to prevent spam
- Duplicate Prevention: Same IP cannot vote for the same restaurant twice
- Vote Tracking: Displays verification progress with visual indicators
- Responsive Design: Mobile-friendly interface with Tailwind CSS
- Live Feedback: Success/error messages with smooth fade animations
- Verification Badges: Visual indicators showing verified (✅) vs pending (
⚠️ ) status - Progress Bars: Real-time verification progress display
- Clear Button: Quick search reset with X button
- Framework: Next.js 16.1.6 (App Router)
- UI Library: React 19.2.3
- Styling: Tailwind CSS 4
- Language: TypeScript
- Runtime: Node.js 25.x
- Framework: Next.js API Routes
- Database: MongoDB 7.0 (Alpine)
- ODM: Mongoose 9.1.5
- Containerization: Docker + Docker Compose
- Production Deployment: Vercel (with MongoDB Atlas)
app/
api/
restaurants/ # POST: Add restaurant with verification
restaurants/search/ # GET: Search with risk assessment
layout.tsx # Root layout
page.tsx # Main UI (form + search)
not-found.tsx # 404 redirect
lib/
mongodb.js # Mongoose connection (cached for serverless)
riskAssessment.js # Risk level calculation logic
models/
Restaurant.js # Mongoose schema with verification tracking
Dockerfile # Multi-stage Next.js build
docker-compose.yml # MongoDB + Next.js orchestration
.dockerignore # Exclude secrets from Docker image
.env.example # Environment variables template
.env.local # Local secrets (NOT committed)
package.json
- Node.js 25.x or later
- npm or yarn
- MongoDB instance (local or MongoDB Atlas)
- Clone and install:
git clone <repository-url>
cd allergy-log
npm install- Set up environment variables:
cp .env.example .env.local- Configure
.env.local:
# MongoDB Atlas connection string
MONGODB_URI="mongodb+srv://<username>:<password>@<cluster>.mongodb.net/restaurant-tracker"
# Node environment
NODE_ENV=development
# API URL (for development)
NEXT_PUBLIC_API_URL=http://localhost:3000npm run devOpen http://localhost:3000 in your browser.
The app will:
- Connect to your MongoDB instance via
MONGODB_URI - Enable hot-reload for code changes
- Show detailed error messages
This project includes a Dockerfile and docker-compose.yml for containerized development and deployment.
docker compose build
docker compose upVisit http://localhost:3000
docker build -t allergy-log:local .
docker run --env MONGODB_URI="${MONGODB_URI}" -p 3000:3000 allergy-log:local# Stop containers (keeps data)
docker compose down
# Stop and remove all data
docker compose down -v
# Remove Docker image
docker image rm allergy-log-app
# Free port 3000
lsof -i :3000To use a real MongoDB Atlas instance:
- Update
MONGODB_URIindocker-compose.yml - Ensure IP whitelist includes your Docker host
- Run:
docker compose up
The Dockerfile uses a two-stage build:
- Builder stage: Compiles TypeScript, builds Next.js app
- Runtime stage: Minimal image with only production dependencies
This reduces final image size and improves security.
Add a new restaurant or vote on verification
Request:
{
"name": "Joe's Diner",
"oilType": "Canola"
}Response (new restaurant):
{
"success": true,
"data": {
"_id": "...",
"name": "joe's diner",
"votes": [{ "oilType": "Canola", "ipAddress": "192.168.1.1", "timestamp": "..." }],
"verified": false,
"consensusOilType": null
},
"message": "Restaurant submitted! ⏳ Waiting for 1 more verification from someone else to confirm."
}Response (verification):
{
"success": true,
"data": {
"verified": true,
"consensusOilType": "Canola",
"votes": [...]
},
"message": "✅ Community Verified! Canola oil confirmed by 2 people"
}Error: Rate Limit (429)
{
"success": false,
"error": "Too many submissions. Please try again later. (Limit: 5 per hour)"
}Error: Duplicate Vote (400)
{
"success": false,
"error": "You have already submitted this restaurant with this oil type"
}Search restaurants with risk assessment
Response:
{
"success": true,
"data": [
{
"name": "joe's diner",
"oilType": "Canola",
"verified": true,
"voteCount": 2,
"verificationStatus": "Community Verified ✅",
"risk": {
"riskLevel": "Low",
"message": "Low Risk - Restaurant uses oils with lower allergenic potential.",
"color": "green"
}
}
],
"count": 1
}| Test | Steps | Expected |
|---|---|---|
| Add Restaurant | Submit form → See success message | Green message, form clears after 5s |
| Unverified Display | Search new restaurant | Shows |
| Verification | Add same restaurant from different IP | Auto-marks verified ✅, 100% progress |
| Duplicate Prevention | Add same restaurant/oil again | Red error: "Already submitted" |
| Rate Limiting | Add 6 restaurants in quick succession | 6th fails: "Too many submissions" |
| Risk Levels | Search different oil types | Colors match: Peanut (red), Canola (green) |
| Data Persistence | Restart server while running locally | All restaurants still searchable |
| Docker Isolation | Verify secrets in Docker | docker compose exec app cat .env → "No such file" |
# Local: npm run dev
# Docker: docker compose up
# Then open http://localhost:3000 and follow checklist above- IP-Based Rate Limiting: 5 submissions per IP per hour
- Duplicate Vote Prevention: Same IP + oil type = rejected
- Verification System: Requires consensus from different IPs
- Environment Variables: Secrets stored in
.env.local(never committed) - Docker Security:
.dockerignoreexcludes.envfiles from image- Multi-stage build removes dev dependencies
- Health checks ensure service readiness
- MongoDB:
- IP whitelist enforced (Atlas)
- Authentication required
- Data persisted in volumes
- Input Validation: Name (2+ chars), oil type (enum)
- Error Handling: Graceful failures with user-friendly messages
- Rate Limiting: Prevents resource exhaustion
# Development
npm run dev # Start dev server (hot reload)
npm run build # Build for production
npm run start # Run production server
npm run lint # Run ESLint
# Docker
docker compose build # Build image
docker compose up # Start services
docker compose down # Stop services
docker compose logs # View logs- Push to GitHub
git add .
git commit -m "feat: add verification and rate limiting"
git push origin main-
Deploy on Vercel
- Connect GitHub repo to Vercel
- Set environment variable:
MONGODB_URI(MongoDB Atlas connection) - Deploy (automatic on push)
-
Verify
- Visit your Vercel deployment URL
- Test all features
- Build and push to registry
docker build -t <username>/allergy-log:latest .
docker push <username>/allergy-log:latest- Deploy (e.g., AWS ECS, Google Cloud Run)
- Use image:
<username>/allergy-log:latest - Set env:
MONGODB_URI - Expose port:
3000
- Use image:
- Core submission and search functionality
- Risk assessment with color-coded badges
- Crowdsourced verification system (2-vote consensus)
- Rate limiting (5 per hour per IP)
- Duplicate vote prevention
- Docker containerization with health checks
- User accounts and authentication
- Admin dashboard for moderation
- Advanced filtering (by cuisine, location)
- Email notifications for verified restaurants
- UI component library setup (shadcn/ui)
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit your changes:
git commit -m "feat: describe your feature" - Push to the branch:
git push origin feature/your-feature - Open a pull request
MIT License - see LICENSE file for details